rasti-db 2.0.1 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -3
  3. data/lib/rasti/db.rb +11 -1
  4. data/lib/rasti/db/collection.rb +10 -1
  5. data/lib/rasti/db/computed_attribute.rb +22 -0
  6. data/lib/rasti/db/nql/filter_condition_strategies/base.rb +17 -0
  7. data/lib/rasti/db/nql/filter_condition_strategies/postgres.rb +21 -0
  8. data/lib/rasti/db/nql/filter_condition_strategies/sqlite.rb +21 -0
  9. data/lib/rasti/db/nql/filter_condition_strategies/types/generic.rb +49 -0
  10. data/lib/rasti/db/nql/filter_condition_strategies/types/pg_array.rb +32 -0
  11. data/lib/rasti/db/nql/filter_condition_strategies/types/sqlite_array.rb +34 -0
  12. data/lib/rasti/db/nql/filter_condition_strategies/unsupported_type_comparison.rb +22 -0
  13. data/lib/rasti/db/nql/nodes/array_content.rb +21 -0
  14. data/lib/rasti/db/nql/nodes/attribute.rb +37 -0
  15. data/lib/rasti/db/nql/nodes/binary_node.rb +4 -0
  16. data/lib/rasti/db/nql/nodes/comparisons/base.rb +15 -1
  17. data/lib/rasti/db/nql/nodes/comparisons/equal.rb +0 -4
  18. data/lib/rasti/db/nql/nodes/comparisons/greater_than.rb +0 -4
  19. data/lib/rasti/db/nql/nodes/comparisons/greater_than_or_equal.rb +0 -4
  20. data/lib/rasti/db/nql/nodes/comparisons/include.rb +0 -4
  21. data/lib/rasti/db/nql/nodes/comparisons/less_than.rb +0 -4
  22. data/lib/rasti/db/nql/nodes/comparisons/less_than_or_equal.rb +0 -4
  23. data/lib/rasti/db/nql/nodes/comparisons/like.rb +0 -4
  24. data/lib/rasti/db/nql/nodes/comparisons/not_equal.rb +0 -4
  25. data/lib/rasti/db/nql/nodes/comparisons/not_include.rb +0 -4
  26. data/lib/rasti/db/nql/nodes/conjunction.rb +2 -2
  27. data/lib/rasti/db/nql/nodes/constants/array.rb +17 -0
  28. data/lib/rasti/db/nql/nodes/constants/base.rb +17 -0
  29. data/lib/rasti/db/nql/nodes/constants/false.rb +1 -1
  30. data/lib/rasti/db/nql/nodes/constants/float.rb +1 -1
  31. data/lib/rasti/db/nql/nodes/constants/integer.rb +1 -1
  32. data/lib/rasti/db/nql/nodes/constants/literal_string.rb +1 -1
  33. data/lib/rasti/db/nql/nodes/constants/string.rb +1 -1
  34. data/lib/rasti/db/nql/nodes/constants/time.rb +1 -1
  35. data/lib/rasti/db/nql/nodes/constants/true.rb +1 -1
  36. data/lib/rasti/db/nql/nodes/disjunction.rb +2 -2
  37. data/lib/rasti/db/nql/nodes/parenthesis_sentence.rb +6 -2
  38. data/lib/rasti/db/nql/nodes/sentence.rb +6 -2
  39. data/lib/rasti/db/nql/syntax.rb +262 -44
  40. data/lib/rasti/db/nql/syntax.treetop +27 -14
  41. data/lib/rasti/db/query.rb +38 -8
  42. data/lib/rasti/db/type_converters/postgres.rb +32 -36
  43. data/lib/rasti/db/type_converters/postgres_types/array.rb +11 -9
  44. data/lib/rasti/db/type_converters/postgres_types/hstore.rb +10 -9
  45. data/lib/rasti/db/type_converters/postgres_types/json.rb +17 -14
  46. data/lib/rasti/db/type_converters/postgres_types/jsonb.rb +17 -14
  47. data/lib/rasti/db/type_converters/sqlite.rb +62 -0
  48. data/lib/rasti/db/type_converters/sqlite_types/array.rb +34 -0
  49. data/lib/rasti/db/version.rb +1 -1
  50. data/rasti-db.gemspec +1 -0
  51. data/spec/collection_spec.rb +8 -0
  52. data/spec/computed_attribute_spec.rb +32 -0
  53. data/spec/minitest_helper.rb +32 -5
  54. data/spec/model_spec.rb +1 -1
  55. data/spec/nql/computed_attributes_spec.rb +29 -0
  56. data/spec/nql/filter_condition_spec.rb +23 -4
  57. data/spec/nql/filter_condition_strategies_spec.rb +112 -0
  58. data/spec/nql/syntax_parser_spec.rb +36 -5
  59. data/spec/query_spec.rb +235 -34
  60. data/spec/type_converters/sqlite_spec.rb +66 -0
  61. metadata +38 -3
  62. data/lib/rasti/db/nql/nodes/field.rb +0 -23
@@ -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.0.1'
3
+ VERSION = '2.3.2'
4
4
  end
5
5
  end
data/rasti-db.gemspec CHANGED
@@ -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
@@ -0,0 +1,32 @@
1
+ require 'minitest_helper'
2
+
3
+ describe 'ComputedAttribute' do
4
+
5
+ it 'Apply Join wiht join attribute must generate correct query' do
6
+ dataset = db[:users]
7
+ computed_attribute = Rasti::DB::ComputedAttribute.new(Sequel[:comments_count][:value]) do |dataset|
8
+ subquery = dataset.db.from(:comments)
9
+ .select(Sequel[:user_id], Sequel.function('count', :id).as(:value))
10
+ .group(:user_id)
11
+ .as(:comments_count)
12
+
13
+ dataset.join_table(:inner, subquery, :user_id => :id)
14
+ end
15
+ expected_query = "SELECT *, `comments_count`.`value` AS 'value' FROM `users` INNER JOIN (SELECT `user_id`, count(`id`) AS 'value' FROM `comments` GROUP BY `user_id`) AS 'comments_count' ON (`comments_count`.`user_id` = `users`.`id`)"
16
+ computed_attribute.apply_join(dataset)
17
+ .select_append(computed_attribute.identifier)
18
+ .sql
19
+ .must_equal expected_query
20
+ end
21
+
22
+ it 'Apply join without join attribute must generate correct query' do
23
+ dataset = db[:people]
24
+ computed_attribute = Rasti::DB::ComputedAttribute.new Sequel.join([:first_name, ' ', :last_name])
25
+ expected_query = "SELECT * FROM `people` WHERE ((`first_name` || ' ' || `last_name`) = 'FULL NAME')"
26
+ computed_attribute.apply_join(dataset)
27
+ .where(computed_attribute.identifier => 'FULL NAME')
28
+ .sql
29
+ .must_equal expected_query
30
+ end
31
+
32
+ end
@@ -10,14 +10,15 @@ 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
- User = Rasti::DB::Model[:id, :name, :posts, :comments, :person]
17
- Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories, :language_id, :language]
18
- Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
17
+ User = Rasti::DB::Model[:id, :name, :posts, :comments, :person, :comments_count]
18
+ Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories, :language_id, :language, :notice, :author]
19
+ Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post, :tags]
19
20
  Category = Rasti::DB::Model[:id, :name, :posts]
20
- Person = Rasti::DB::Model[:document_number, :first_name, :last_name, :birth_date, :user_id, :user, :languages]
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]
22
23
 
23
24
 
@@ -25,6 +26,18 @@ class Users < Rasti::DB::Collection
25
26
  one_to_many :posts
26
27
  one_to_many :comments
27
28
  one_to_one :person
29
+
30
+ computed_attribute :comments_count do
31
+ Rasti::DB::ComputedAttribute.new(Sequel[:comments_count][:value]) do |dataset|
32
+ subquery = dataset.db.from(:comments)
33
+ .select(Sequel[:user_id], Sequel.function('count', :id).as(:value))
34
+ .group(:user_id)
35
+ .as(:comments_count)
36
+
37
+ dataset.join_table(:inner, subquery, :user_id => :id)
38
+ end
39
+ end
40
+
28
41
  end
29
42
 
30
43
  class Posts < Rasti::DB::Collection
@@ -47,6 +60,15 @@ class Posts < Rasti::DB::Collection
47
60
  .distinct
48
61
  end
49
62
  end
63
+
64
+ computed_attribute :notice do
65
+ Rasti::DB::ComputedAttribute.new Sequel.join([:title, ': ', :body])
66
+ end
67
+
68
+ computed_attribute :author do
69
+ Rasti::DB::ComputedAttribute.new Sequel[:user]
70
+ end
71
+
50
72
  end
51
73
 
52
74
  class Comments < Rasti::DB::Collection
@@ -73,6 +95,10 @@ class People < Rasti::DB::Collection
73
95
 
74
96
  many_to_one :user
75
97
  many_to_many :languages
98
+
99
+ computed_attribute :full_name do |db|
100
+ Rasti::DB::ComputedAttribute.new Sequel.join([:first_name, ' ', :last_name])
101
+ end
76
102
  end
77
103
 
78
104
  class Languages < Rasti::DB::Collection
@@ -123,6 +149,7 @@ class Minitest::Spec
123
149
  db.create_table :comments do
124
150
  primary_key :id
125
151
  String :text, null: false
152
+ String :tags, default: Sequel.lit("'[]'")
126
153
  foreign_key :user_id, :users, null: false, index: true
127
154
  foreign_key :post_id, :posts, null: false, index: true
128
155
  end
data/spec/model_spec.rb CHANGED
@@ -42,7 +42,7 @@ describe 'Model' do
42
42
  describe 'To String' do
43
43
 
44
44
  it 'Class' do
45
- User.to_s.must_equal 'User[id, name, posts, comments, person]'
45
+ User.to_s.must_equal 'User[id, name, posts, comments, person, comments_count]'
46
46
  end
47
47
 
48
48
  it 'Instance' do
@@ -0,0 +1,29 @@
1
+ require 'minitest_helper'
2
+
3
+ describe 'NQL::ComputedAttributes' do
4
+
5
+ let(:parser) { Rasti::DB::NQL::SyntaxParser.new }
6
+
7
+ def parse(expression)
8
+ parser.parse expression
9
+ end
10
+
11
+ it 'must have one computed attributes' do
12
+ tree = parse 'notice = any notice'
13
+
14
+ tree.computed_attributes(Posts).must_equal [:notice]
15
+ end
16
+
17
+ it 'must have multiple computed attributes' do
18
+ tree = parse 'notice = any notice & (author: anonym | title = good morning)'
19
+
20
+ tree.computed_attributes(Posts).must_equal [:notice, :author]
21
+ end
22
+
23
+ it 'must have not repeated computed attributes when expression have it' do
24
+ tree = parse 'notice = Hi | notice = Bye'
25
+
26
+ tree.computed_attributes(Posts).must_equal [:notice]
27
+ end
28
+
29
+ end
@@ -4,9 +4,11 @@ describe 'NQL::FilterCondition' do
4
4
 
5
5
  let(:parser) { Rasti::DB::NQL::SyntaxParser.new }
6
6
 
7
+ let(:collection_class) { Rasti::DB::Collection }
8
+
7
9
  def filter_condition(expression)
8
10
  tree = parser.parse expression
9
- tree.filter_condition
11
+ tree.filter_condition(collection_class)
10
12
  end
11
13
 
12
14
  def assert_identifier(identifier, expected_value)
@@ -17,13 +19,30 @@ describe 'NQL::FilterCondition' do
17
19
  def assert_comparison(filter, expected_left, expected_comparator, expected_right)
18
20
  filter.must_be_instance_of Sequel::SQL::BooleanExpression
19
21
  filter.op.must_equal expected_comparator.to_sym
20
-
22
+
21
23
  left, right = filter.args
22
24
  assert_identifier left, expected_left
23
25
 
24
26
  right.must_equal expected_right
25
27
  end
26
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
+
27
46
  describe 'Comparison' do
28
47
 
29
48
  it 'must create filter from expression with <' do
@@ -95,7 +114,7 @@ describe 'NQL::FilterCondition' do
95
114
 
96
115
  end
97
116
 
98
- it 'must create filter from expression with field with multiple tables' do
117
+ it 'must create filter from expression with attribute with multiple tables' do
99
118
  filter = filter_condition 'table_one.table_two.column = test'
100
119
  identifier, value = filter.first
101
120
 
@@ -132,7 +151,7 @@ describe 'NQL::FilterCondition' do
132
151
 
133
152
  filter.must_be_instance_of Sequel::SQL::BooleanExpression
134
153
  filter.op.must_equal :AND
135
-
154
+
136
155
  major_expression, and_expression = filter.args
137
156
  assert_comparison major_expression, 'column_one', '>', 1
138
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