rasti-db 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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