rasti-db 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rasti/db.rb +11 -1
  3. data/lib/rasti/db/nql/filter_condition_strategies/base.rb +17 -0
  4. data/lib/rasti/db/nql/filter_condition_strategies/postgres.rb +21 -0
  5. data/lib/rasti/db/nql/filter_condition_strategies/sqlite.rb +21 -0
  6. data/lib/rasti/db/nql/filter_condition_strategies/types/generic.rb +49 -0
  7. data/lib/rasti/db/nql/filter_condition_strategies/types/pg_array.rb +32 -0
  8. data/lib/rasti/db/nql/filter_condition_strategies/types/sqlite_array.rb +34 -0
  9. data/lib/rasti/db/nql/filter_condition_strategies/unsupported_type_comparison.rb +22 -0
  10. data/lib/rasti/db/nql/nodes/array_content.rb +21 -0
  11. data/lib/rasti/db/nql/nodes/comparisons/base.rb +10 -0
  12. data/lib/rasti/db/nql/nodes/comparisons/equal.rb +0 -4
  13. data/lib/rasti/db/nql/nodes/comparisons/greater_than.rb +0 -4
  14. data/lib/rasti/db/nql/nodes/comparisons/greater_than_or_equal.rb +0 -4
  15. data/lib/rasti/db/nql/nodes/comparisons/include.rb +0 -4
  16. data/lib/rasti/db/nql/nodes/comparisons/less_than.rb +0 -4
  17. data/lib/rasti/db/nql/nodes/comparisons/less_than_or_equal.rb +0 -4
  18. data/lib/rasti/db/nql/nodes/comparisons/like.rb +0 -4
  19. data/lib/rasti/db/nql/nodes/comparisons/not_equal.rb +0 -4
  20. data/lib/rasti/db/nql/nodes/comparisons/not_include.rb +0 -4
  21. data/lib/rasti/db/nql/nodes/constants/array.rb +17 -0
  22. data/lib/rasti/db/nql/nodes/constants/base.rb +17 -0
  23. data/lib/rasti/db/nql/nodes/constants/false.rb +1 -1
  24. data/lib/rasti/db/nql/nodes/constants/float.rb +1 -1
  25. data/lib/rasti/db/nql/nodes/constants/integer.rb +1 -1
  26. data/lib/rasti/db/nql/nodes/constants/literal_string.rb +1 -1
  27. data/lib/rasti/db/nql/nodes/constants/string.rb +1 -1
  28. data/lib/rasti/db/nql/nodes/constants/time.rb +1 -1
  29. data/lib/rasti/db/nql/nodes/constants/true.rb +1 -1
  30. data/lib/rasti/db/nql/syntax.rb +229 -11
  31. data/lib/rasti/db/nql/syntax.treetop +24 -11
  32. data/lib/rasti/db/type_converters/sqlite.rb +62 -0
  33. data/lib/rasti/db/type_converters/sqlite_types/array.rb +34 -0
  34. data/lib/rasti/db/version.rb +1 -1
  35. data/rasti-db.gemspec +1 -0
  36. data/spec/collection_spec.rb +8 -0
  37. data/spec/minitest_helper.rb +4 -2
  38. data/spec/nql/filter_condition_spec.rb +19 -2
  39. data/spec/nql/filter_condition_strategies_spec.rb +112 -0
  40. data/spec/nql/syntax_parser_spec.rb +24 -0
  41. data/spec/query_spec.rb +89 -0
  42. data/spec/type_converters/sqlite_spec.rb +66 -0
  43. metadata +32 -2
@@ -0,0 +1,62 @@
1
+ module Rasti
2
+ module DB
3
+ module TypeConverters
4
+ class SQLite
5
+
6
+ CONVERTERS = [SQLiteTypes::Array]
7
+
8
+ @to_db_mapping = {}
9
+
10
+ class << self
11
+
12
+ def to_db(db, collection_name, attribute_name, value)
13
+ to_db_mapping = to_db_mapping_for db, collection_name
14
+
15
+ if to_db_mapping.key? attribute_name
16
+ to_db_mapping[attribute_name][:converter].to_db value
17
+ else
18
+ value
19
+ end
20
+ end
21
+
22
+ def from_db(object)
23
+ converter = find_converter_from_db object
24
+ if !converter.nil?
25
+ converter.from_db object
26
+ else
27
+ object
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def to_db_mapping_for(db, collection_name)
34
+ key = [db.opts[:database], collection_name]
35
+
36
+ @to_db_mapping[key] ||= begin
37
+ columns = Hash[db.schema(collection_name)]
38
+
39
+ columns.each_with_object({}) do |(name, schema), hash|
40
+ CONVERTERS.each do |converter|
41
+ unless hash.key? name
42
+ match = converter.column_type_regex.match schema[:db_type]
43
+
44
+ hash[name] = { converter: converter } if match
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def find_converter_from_db(object)
52
+ CONVERTERS.find do |converter|
53
+ converter.respond_for? object
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ module Rasti
2
+ module DB
3
+ module TypeConverters
4
+ module SQLiteTypes
5
+ class Array
6
+
7
+ class << self
8
+
9
+ def column_type_regex
10
+ /^([a-z]+)\[\]$/
11
+ end
12
+
13
+ def to_db(values)
14
+ JSON.dump(values)
15
+ end
16
+
17
+ def respond_for?(object)
18
+ parsed = JSON.parse object
19
+ object == to_db(parsed)
20
+ rescue
21
+ false
22
+ end
23
+
24
+ def from_db(object)
25
+ JSON.parse object
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  module Rasti
2
2
  module DB
3
- VERSION = '2.2.0'
3
+ VERSION = '2.3.0'
4
4
  end
5
5
  end
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'multi_require', '~> 1.0'
27
27
  spec.add_runtime_dependency 'hierarchical_graph', '~> 1.0'
28
28
  spec.add_runtime_dependency 'hash_ext', '~> 0.5'
29
+ spec.add_runtime_dependency 'inflecto', '~> 0.0'
29
30
 
30
31
  spec.add_development_dependency 'rake', '~> 12.3'
31
32
  spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
@@ -2,6 +2,14 @@ require 'minitest_helper'
2
2
 
3
3
  describe 'Collection' do
4
4
 
5
+ before do
6
+ Rasti::DB.type_converters = [Rasti::DB::TypeConverters::TimeInZone]
7
+ end
8
+
9
+ after do
10
+ Rasti::DB.type_converters = [Rasti::DB::TypeConverters::TimeInZone, Rasti::DB::TypeConverters::SQLite]
11
+ end
12
+
5
13
  describe 'Specification' do
6
14
 
7
15
  it 'Implicit' do
@@ -10,12 +10,13 @@ require 'sequel/extensions/pg_array'
10
10
  require 'sequel/extensions/pg_json'
11
11
 
12
12
  Rasti::DB.configure do |config|
13
- config.type_converters = [Rasti::DB::TypeConverters::TimeInZone]
13
+ config.type_converters = [Rasti::DB::TypeConverters::TimeInZone, Rasti::DB::TypeConverters::SQLite]
14
+ config.nql_filter_condition_strategy = Rasti::DB::NQL::FilterConditionStrategies::SQLite.new
14
15
  end
15
16
 
16
17
  User = Rasti::DB::Model[:id, :name, :posts, :comments, :person, :comments_count]
17
18
  Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories, :language_id, :language, :notice, :author]
18
- Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
19
+ Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post, :tags]
19
20
  Category = Rasti::DB::Model[:id, :name, :posts]
20
21
  Person = Rasti::DB::Model[:document_number, :first_name, :last_name, :birth_date, :user_id, :user, :languages, :full_name]
21
22
  Language = Rasti::DB::Model[:id, :name, :people]
@@ -148,6 +149,7 @@ class Minitest::Spec
148
149
  db.create_table :comments do
149
150
  primary_key :id
150
151
  String :text, null: false
152
+ String :tags, default: Sequel.lit("'[]'")
151
153
  foreign_key :user_id, :users, null: false, index: true
152
154
  foreign_key :post_id, :posts, null: false, index: true
153
155
  end
@@ -19,13 +19,30 @@ describe 'NQL::FilterCondition' do
19
19
  def assert_comparison(filter, expected_left, expected_comparator, expected_right)
20
20
  filter.must_be_instance_of Sequel::SQL::BooleanExpression
21
21
  filter.op.must_equal expected_comparator.to_sym
22
-
22
+
23
23
  left, right = filter.args
24
24
  assert_identifier left, expected_left
25
25
 
26
26
  right.must_equal expected_right
27
27
  end
28
28
 
29
+ describe 'None Filter Condition Strategy Validation' do
30
+
31
+ before do
32
+ Rasti::DB.nql_filter_condition_strategy = nil
33
+ end
34
+
35
+ after do
36
+ Rasti::DB.nql_filter_condition_strategy = Rasti::DB::NQL::FilterConditionStrategies::SQLite.new
37
+ end
38
+
39
+ it 'must raise error' do
40
+ error = proc { filter_condition 'column = value' }.must_raise RuntimeError
41
+ error.message.must_equal 'Undefined Rasti::DB.nql_filter_condition_strategy'
42
+ end
43
+
44
+ end
45
+
29
46
  describe 'Comparison' do
30
47
 
31
48
  it 'must create filter from expression with <' do
@@ -134,7 +151,7 @@ describe 'NQL::FilterCondition' do
134
151
 
135
152
  filter.must_be_instance_of Sequel::SQL::BooleanExpression
136
153
  filter.op.must_equal :AND
137
-
154
+
138
155
  major_expression, and_expression = filter.args
139
156
  assert_comparison major_expression, 'column_one', '>', 1
140
157
 
@@ -0,0 +1,112 @@
1
+ require 'minitest_helper'
2
+
3
+ describe 'NQL::FilterConditionStrategies' do
4
+
5
+ let(:comments_query) { Rasti::DB::Query.new collection_class: Comments, dataset: db[:comments], environment: environment }
6
+
7
+ def sqls_where(query)
8
+ "#<Rasti::DB::Query: \"SELECT `comments`.* FROM `comments` WHERE (#{query})\">"
9
+ end
10
+
11
+ def sqls_where_not(query)
12
+ "#<Rasti::DB::Query: \"SELECT `comments`.* FROM `comments` WHERE NOT (#{query})\">"
13
+ end
14
+
15
+ def nql_s(nql_query)
16
+ comments_query.nql(nql_query).to_s
17
+ end
18
+
19
+ describe 'Generic' do
20
+
21
+ it 'Equal' do
22
+ nql_s('text = hola').must_equal sqls_where("`comments`.`text` = 'hola'")
23
+ end
24
+
25
+ it 'Not Equal' do
26
+ nql_s('text != hola').must_equal sqls_where("`comments`.`text` != 'hola'")
27
+ end
28
+
29
+ it 'Greather Than' do
30
+ nql_s('id > 1').must_equal sqls_where("`comments`.`id` > 1")
31
+ end
32
+
33
+ it 'Greather Than or Equal' do
34
+ nql_s('id >= 1').must_equal sqls_where("`comments`.`id` >= 1")
35
+ end
36
+
37
+ it 'Less Than' do
38
+ nql_s('id < 1').must_equal sqls_where("`comments`.`id` < 1")
39
+ end
40
+
41
+ it 'Less Than or Equal' do
42
+ nql_s('id <= 1').must_equal sqls_where("`comments`.`id` <= 1")
43
+ end
44
+
45
+ it 'Like' do
46
+ nql_s('text ~ hola').must_equal sqls_where("UPPER(`comments`.`text`) LIKE UPPER('hola') ESCAPE '\\'")
47
+ end
48
+
49
+ it 'Include' do
50
+ nql_s('text: hola').must_equal sqls_where("UPPER(`comments`.`text`) LIKE UPPER('%hola%') ESCAPE '\\'")
51
+ end
52
+
53
+ it 'Not Include' do
54
+ nql_s('text!: hola').must_equal sqls_where_not("UPPER(`comments`.`text`) LIKE UPPER('%hola%') ESCAPE '\\'")
55
+ end
56
+
57
+ end
58
+
59
+ describe 'SQLite Array' do
60
+
61
+ it 'Equal' do
62
+ nql_s('tags = [notice]').must_equal sqls_where("`comments`.`tags` = '[\"notice\"]'")
63
+ end
64
+
65
+ it 'Not Equal' do
66
+ nql_s('tags != [notice]').must_equal sqls_where_not("`comments`.`tags` LIKE '%\"notice\"%' ESCAPE '\\'")
67
+ end
68
+
69
+ it 'Like' do
70
+ nql_s('tags ~ [notice]').must_equal sqls_where("`comments`.`tags` LIKE '%notice%' ESCAPE '\\'")
71
+ end
72
+
73
+ it 'Include' do
74
+ nql_s('tags: [notice]').must_equal sqls_where("`comments`.`tags` LIKE '%\"notice\"%' ESCAPE '\\'")
75
+ end
76
+
77
+ it 'Not Include' do
78
+ nql_s('tags!: [notice]').must_equal sqls_where_not("`comments`.`tags` LIKE '%\"notice\"%' ESCAPE '\\'")
79
+ end
80
+
81
+ end
82
+
83
+ describe 'Postgres Array' do
84
+
85
+ before do
86
+ Rasti::DB.nql_filter_condition_strategy = Rasti::DB::NQL::FilterConditionStrategies::Postgres.new
87
+ Sequel.extension :pg_array_ops
88
+ end
89
+
90
+ after do
91
+ Rasti::DB.nql_filter_condition_strategy = Rasti::DB::NQL::FilterConditionStrategies::SQLite.new
92
+ end
93
+
94
+ it 'Equal' do
95
+ nql_s('tags = [notice]').must_equal sqls_where("(`comments`.`tags` @> ARRAY['notice']) AND (`comments`.`tags` <@ ARRAY['notice'])")
96
+ end
97
+
98
+ it 'Not Equal' do
99
+ nql_s('tags != [notice]').must_equal sqls_where("NOT (`comments`.`tags` @> ARRAY['notice']) OR NOT (`comments`.`tags` <@ ARRAY['notice'])")
100
+ end
101
+
102
+ it 'Include' do
103
+ nql_s('tags: [notice]').must_equal sqls_where("`comments`.`tags` && ARRAY['notice']")
104
+ end
105
+
106
+ it 'Not Include' do
107
+ nql_s('tags!: [notice]').must_equal sqls_where_not("`comments`.`tags` && ARRAY['notice']")
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -213,4 +213,28 @@ describe 'NQL::SyntaxParser' do
213
213
 
214
214
  end
215
215
 
216
+ describe 'Array' do
217
+
218
+ it 'must parse array with one element' do
219
+ tree = parse 'column = [ a ]'
220
+ tree.proposition.argument.value.must_equal ['a']
221
+ end
222
+
223
+ it 'must parse array with many elements' do
224
+ tree = parse 'column = [ a, b, c ]'
225
+ tree.proposition.argument.value.must_equal ['a', 'b', 'c']
226
+ end
227
+
228
+ it 'must parse array respecting content types' do
229
+ tree = parse 'column = [ true, 12:00, 1.1, 1, "literal string", string ]'
230
+ tree.proposition.argument.value.must_equal [true, Timing::TimeInZone.parse('12:00').to_s, 1.1, 1, "literal string", 'string']
231
+ end
232
+
233
+ it 'must parse array with literal string allowing reserved characters in conflict with array' do
234
+ tree = parse 'column = [ ",", "[", "]", "[,", "],", "[,]", "simple literal" ]'
235
+ tree.proposition.argument.value.must_equal [ ',', '[', ']', '[,', '],', '[,]', 'simple literal' ]
236
+ end
237
+
238
+ end
239
+
216
240
  end
@@ -462,6 +462,95 @@ describe 'Query' do
462
462
 
463
463
  end
464
464
 
465
+ describe 'Filter Array' do
466
+
467
+ def filter_condition_must_raise(comparison_symbol, comparison_name)
468
+ error = proc { comments_query.nql("tags #{comparison_symbol} [fake, notice]") }.must_raise Rasti::DB::NQL::FilterConditionStrategies::UnsupportedTypeComparison
469
+ error.argument_type.must_equal Rasti::DB::NQL::FilterConditionStrategies::Types::SQLiteArray
470
+ error.comparison_name.must_equal comparison_name
471
+ error.message.must_equal "Unsupported comparison #{comparison_name} for Rasti::DB::NQL::FilterConditionStrategies::Types::SQLiteArray"
472
+ end
473
+
474
+ it 'Must raise exception from not supported methods' do
475
+ comparisons = {
476
+ greater_than: '>',
477
+ greater_than_or_equal: '>=',
478
+ less_than: '<',
479
+ less_than_or_equal: '<='
480
+ }
481
+
482
+ comparisons.each do |name, symbol|
483
+ filter_condition_must_raise symbol, name
484
+ end
485
+ end
486
+
487
+ it 'Included any of these elements' do
488
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake","notice"]'
489
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice 2', tags: '["notice"]'
490
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice 3', tags: '["fake_notice"]'
491
+ expected_comments = [
492
+ Comment.new(id: 4, text: 'fake notice', tags: ['fake','notice'], user_id: 5, post_id: 1),
493
+ Comment.new(id: 5, text: 'fake notice 2', tags: ['notice'], user_id: 5, post_id: 1)
494
+ ]
495
+
496
+ comments_query.nql('tags: [ fake, notice ]')
497
+ .all
498
+ .must_equal expected_comments
499
+ end
500
+
501
+ it 'Included exactly all these elements' do
502
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake","notice"]'
503
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice 2', tags: '["notice"]'
504
+ comments_query.nql('tags = [fake, notice]')
505
+ .all
506
+ .must_equal [Comment.new(id: 4, text: 'fake notice', tags: ['fake','notice'], user_id: 5, post_id: 1)]
507
+ end
508
+
509
+ it 'Not included anyone of these elements' do
510
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake","notice"]'
511
+ db[:comments].insert post_id: 1, user_id: 5, text: 'Good notice!', tags: '["good"]'
512
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake"]'
513
+ expected_comments = [
514
+ Comment.new(id: 1, text: 'Comment 1', tags: [], user_id: 5, post_id: 1),
515
+ Comment.new(id: 2, text: 'Comment 2', tags: [], user_id: 7, post_id: 1),
516
+ Comment.new(id: 3, text: 'Comment 3', tags: [], user_id: 2, post_id: 2),
517
+ Comment.new(id: 5, text: 'Good notice!', tags: ['good'], user_id: 5, post_id: 1)
518
+ ]
519
+ comments_query.nql('tags !: [fake, notice]')
520
+ .all
521
+ .must_equal expected_comments
522
+ end
523
+
524
+ it 'Not include any of these elements' do
525
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake","notice"]'
526
+ db[:comments].insert post_id: 1, user_id: 5, text: 'Good notice!', tags: '["good"]'
527
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake"]'
528
+ expected_comments = [
529
+ Comment.new(id: 1, text: 'Comment 1', tags: '[]', user_id: 5, post_id: 1),
530
+ Comment.new(id: 2, text: 'Comment 2', tags: '[]', user_id: 7, post_id: 1),
531
+ Comment.new(id: 3, text: 'Comment 3', tags: '[]', user_id: 2, post_id: 2),
532
+ Comment.new(id: 5, text: 'Good notice!', tags: ['good'], user_id: 5, post_id: 1),
533
+ Comment.new(id: 6, text: 'fake notice', tags: ['fake'], user_id: 5, post_id: 1)
534
+ ]
535
+ comments_query.nql('tags != [fake, notice]')
536
+ .all
537
+ .must_equal expected_comments
538
+ end
539
+
540
+ it 'Include any like these elements' do
541
+ db[:comments].insert post_id: 1, user_id: 5, text: 'fake notice', tags: '["fake","notice"]'
542
+ db[:comments].insert post_id: 1, user_id: 5, text: 'this is a fake notice!', tags: '["fake_notice"]'
543
+ expected_comments = [
544
+ Comment.new(id: 4, text: 'fake notice', tags: ['fake','notice'], user_id: 5, post_id: 1),
545
+ Comment.new(id: 5, text: 'this is a fake notice!', tags: ['fake_notice'], user_id: 5, post_id: 1)
546
+ ]
547
+ comments_query.nql('tags ~ [fake]')
548
+ .all
549
+ .must_equal expected_comments
550
+ end
551
+
552
+ end
553
+
465
554
  end
466
555
 
467
556
  end
@@ -0,0 +1,66 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::DB::TypeConverters::SQLite do
4
+
5
+ let(:type_converter) { Rasti::DB::TypeConverters::SQLite }
6
+
7
+ let(:sqlite) do
8
+ Object.new.tap do |sqlite|
9
+
10
+ def sqlite.opts
11
+ {
12
+ database: 'database'
13
+ }
14
+ end
15
+
16
+ def sqlite.schema(table_name, opts={})
17
+ [
18
+ [:text_array, {db_type: 'text[]'}],
19
+ ]
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ describe 'Default' do
26
+
27
+ it 'must not change value in to_db if column not found in mapping' do
28
+ string = type_converter.to_db sqlite, :table_name, :column, "hola"
29
+ string.class.must_equal String
30
+ string.must_equal "hola"
31
+ end
32
+
33
+ it 'must not change value in from_db if class not found in mapping' do
34
+ string = type_converter.from_db "hola"
35
+ string.class.must_equal String
36
+ string.must_equal "hola"
37
+ end
38
+
39
+ end
40
+
41
+ describe 'Array' do
42
+
43
+ describe 'To DB' do
44
+
45
+ it 'must transform Array to SQLiteArray' do
46
+ sqlite_array = type_converter.to_db sqlite, :table_name, :text_array, ['a', 'b', 'c']
47
+ sqlite_array.class.must_equal String
48
+ sqlite_array.must_equal '["a","b","c"]'
49
+ end
50
+
51
+ end
52
+
53
+ describe 'From DB' do
54
+
55
+ it 'must transform SQLiteArray to Array' do
56
+ sqlite_array = '["a","b","c"]'
57
+ array = type_converter.from_db sqlite_array
58
+ array.class.must_equal Array
59
+ array.must_equal ['a', 'b', 'c']
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end