rasti-db 1.4.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -3
  3. data/README.md +88 -24
  4. data/lib/rasti/db.rb +2 -1
  5. data/lib/rasti/db/collection.rb +79 -46
  6. data/lib/rasti/db/computed_attribute.rb +22 -0
  7. data/lib/rasti/db/data_source.rb +18 -0
  8. data/lib/rasti/db/environment.rb +32 -0
  9. data/lib/rasti/db/nql/nodes/attribute.rb +37 -0
  10. data/lib/rasti/db/nql/nodes/binary_node.rb +4 -0
  11. data/lib/rasti/db/nql/nodes/comparisons/base.rb +5 -1
  12. data/lib/rasti/db/nql/nodes/comparisons/equal.rb +2 -2
  13. data/lib/rasti/db/nql/nodes/comparisons/greater_than.rb +2 -2
  14. data/lib/rasti/db/nql/nodes/comparisons/greater_than_or_equal.rb +2 -2
  15. data/lib/rasti/db/nql/nodes/comparisons/include.rb +2 -2
  16. data/lib/rasti/db/nql/nodes/comparisons/less_than.rb +2 -2
  17. data/lib/rasti/db/nql/nodes/comparisons/less_than_or_equal.rb +2 -2
  18. data/lib/rasti/db/nql/nodes/comparisons/like.rb +2 -2
  19. data/lib/rasti/db/nql/nodes/comparisons/not_equal.rb +2 -2
  20. data/lib/rasti/db/nql/nodes/comparisons/not_include.rb +2 -2
  21. data/lib/rasti/db/nql/nodes/conjunction.rb +2 -2
  22. data/lib/rasti/db/nql/nodes/disjunction.rb +2 -2
  23. data/lib/rasti/db/nql/nodes/parenthesis_sentence.rb +6 -2
  24. data/lib/rasti/db/nql/nodes/sentence.rb +6 -2
  25. data/lib/rasti/db/nql/syntax.rb +33 -33
  26. data/lib/rasti/db/nql/syntax.treetop +12 -12
  27. data/lib/rasti/db/query.rb +107 -43
  28. data/lib/rasti/db/relations/base.rb +22 -8
  29. data/lib/rasti/db/relations/graph.rb +129 -0
  30. data/lib/rasti/db/relations/many_to_many.rb +58 -24
  31. data/lib/rasti/db/relations/many_to_one.rb +17 -12
  32. data/lib/rasti/db/relations/one_to_many.rb +27 -16
  33. data/lib/rasti/db/version.rb +1 -1
  34. data/rasti-db.gemspec +3 -7
  35. data/spec/collection_spec.rb +223 -52
  36. data/spec/computed_attribute_spec.rb +32 -0
  37. data/spec/minitest_helper.rb +76 -15
  38. data/spec/model_spec.rb +4 -2
  39. data/spec/nql/computed_attributes_spec.rb +29 -0
  40. data/spec/nql/filter_condition_spec.rb +4 -2
  41. data/spec/nql/syntax_parser_spec.rb +12 -5
  42. data/spec/query_spec.rb +319 -85
  43. data/spec/relations_spec.rb +27 -7
  44. metadata +41 -7
  45. data/lib/rasti/db/helpers.rb +0 -16
  46. data/lib/rasti/db/nql/nodes/field.rb +0 -23
  47. data/lib/rasti/db/relations/graph_builder.rb +0 -60
@@ -42,44 +42,44 @@ module Rasti
42
42
  comparison_equal
43
43
  end
44
44
 
45
- rule field
46
- _tables:(table:field_name '.')* _column:field_name <Nodes::Field>
45
+ rule attribute
46
+ _tables:(table:attribute_name '.')* _column:attribute_name <Nodes::Attribute>
47
47
  end
48
48
 
49
49
  rule comparison_include
50
- field:field space* comparator:':' space* argument:basic <Nodes::Comparisons::Include>
50
+ attribute:attribute space* comparator:':' space* argument:basic <Nodes::Comparisons::Include>
51
51
  end
52
52
 
53
53
  rule comparison_not_include
54
- field:field space* comparator:'!:' space* argument:basic <Nodes::Comparisons::NotInclude>
54
+ attribute:attribute space* comparator:'!:' space* argument:basic <Nodes::Comparisons::NotInclude>
55
55
  end
56
56
 
57
57
  rule comparison_like
58
- field:field space* comparator:'~' space* argument:basic <Nodes::Comparisons::Like>
58
+ attribute:attribute space* comparator:'~' space* argument:basic <Nodes::Comparisons::Like>
59
59
  end
60
60
 
61
61
  rule comparison_greater_than
62
- field:field space* comparator:'>' space* argument:basic <Nodes::Comparisons::GreaterThan>
62
+ attribute:attribute space* comparator:'>' space* argument:basic <Nodes::Comparisons::GreaterThan>
63
63
  end
64
64
 
65
65
  rule comparison_greater_than_or_equal
66
- field:field space* comparator:'>=' space* argument:basic <Nodes::Comparisons::GreaterThanOrEqual>
66
+ attribute:attribute space* comparator:'>=' space* argument:basic <Nodes::Comparisons::GreaterThanOrEqual>
67
67
  end
68
68
 
69
69
  rule comparison_less_than
70
- field:field space* comparator:'<' space* argument:basic <Nodes::Comparisons::LessThan>
70
+ attribute:attribute space* comparator:'<' space* argument:basic <Nodes::Comparisons::LessThan>
71
71
  end
72
72
 
73
73
  rule comparison_less_than_or_equal
74
- field:field space* comparator:'<=' space* argument:basic <Nodes::Comparisons::LessThanOrEqual>
74
+ attribute:attribute space* comparator:'<=' space* argument:basic <Nodes::Comparisons::LessThanOrEqual>
75
75
  end
76
76
 
77
77
  rule comparison_not_equal
78
- field:field space* comparator:'!=' space* argument:basic <Nodes::Comparisons::NotEqual>
78
+ attribute:attribute space* comparator:'!=' space* argument:basic <Nodes::Comparisons::NotEqual>
79
79
  end
80
80
 
81
81
  rule comparison_equal
82
- field:field space* comparator:'=' space* argument:basic <Nodes::Comparisons::Equal>
82
+ attribute:attribute space* comparator:'=' space* argument:basic <Nodes::Comparisons::Equal>
83
83
  end
84
84
 
85
85
  rule basic
@@ -95,7 +95,7 @@ module Rasti
95
95
  [\s\t\n]
96
96
  end
97
97
 
98
- rule field_name
98
+ rule attribute_name
99
99
  [a-z_]+
100
100
  end
101
101
 
@@ -5,13 +5,18 @@ module Rasti
5
5
  DATASET_CHAINED_METHODS = [:where, :exclude, :or, :order, :reverse_order, :limit, :offset].freeze
6
6
 
7
7
  include Enumerable
8
- include Helpers::WithSchema
9
8
 
10
- def initialize(collection_class, dataset, relations=[], schema=nil)
9
+ def initialize(environment:, collection_class:, dataset:, relations_graph:nil)
10
+ @environment = environment
11
11
  @collection_class = collection_class
12
- @dataset = dataset
13
- @relations = relations
14
- @schema = schema
12
+ @dataset = dataset.qualify collection_class.collection_name
13
+ @relations_graph = relations_graph || Relations::Graph.new(environment, collection_class)
14
+ end
15
+
16
+ DATASET_CHAINED_METHODS.each do |method|
17
+ define_method method do |*args, &block|
18
+ build_query dataset: dataset.public_send(method, *args, &block)
19
+ end
15
20
  end
16
21
 
17
22
  def raw
@@ -19,7 +24,7 @@ module Rasti
19
24
  end
20
25
 
21
26
  def pluck(*attributes)
22
- ds = dataset.select(*attributes.map { |attr| Sequel[collection_class.collection_name][attr] })
27
+ ds = dataset.select(*attributes.map { |a| Sequel[collection_class.collection_name][a] })
23
28
  attributes.count == 1 ? ds.map { |r| r[attributes.first] } : ds.map(&:values)
24
29
  end
25
30
 
@@ -27,38 +32,73 @@ module Rasti
27
32
  pluck collection_class.primary_key
28
33
  end
29
34
 
35
+ def select_attributes(*attributes)
36
+ build_query dataset: dataset.select(*attributes.map { |a| Sequel[collection_class.collection_name][a] })
37
+ end
38
+
39
+ def exclude_attributes(*excluded_attributes)
40
+ attributes = collection_class.collection_attributes - excluded_attributes
41
+ select_attributes(*attributes)
42
+ end
43
+
44
+ def all_attributes
45
+ build_query dataset: dataset.select_all(collection_class.collection_name)
46
+ end
47
+
48
+ def select_graph_attributes(selected_attributes)
49
+ build_query relations_graph: relations_graph.merge(selected_attributes: selected_attributes)
50
+ end
51
+
52
+ def exclude_graph_attributes(excluded_attributes)
53
+ build_query relations_graph: relations_graph.merge(excluded_attributes: excluded_attributes)
54
+ end
55
+
56
+ def all_graph_attributes(*relations)
57
+ build_query relations_graph: relations_graph.with_all_attributes_for(relations)
58
+ end
59
+
60
+ def select_computed_attributes(*computed_attributes)
61
+ ds = computed_attributes.inject(dataset) do |ds, name|
62
+ computed_attribute = collection_class.computed_attributes[name]
63
+ computed_attribute.apply_join(ds).select_append(computed_attribute.identifier.as(name))
64
+ end
65
+ build_query dataset: ds
66
+ end
67
+
30
68
  def all
31
- with_graph(dataset.all).map do |row|
69
+ with_graph(dataset.all).map do |row|
32
70
  collection_class.model.new row
33
71
  end
34
72
  end
35
73
  alias_method :to_a, :all
36
74
 
37
- def each(&block)
38
- all.each(&block)
75
+ def each(batch_size:nil, &block)
76
+ if batch_size.nil?
77
+ all.each(&block)
78
+ else
79
+ each_batch(size: batch_size) do |models|
80
+ models.each { |model| block.call model }
81
+ end
82
+ end
39
83
  end
40
84
 
41
- DATASET_CHAINED_METHODS.each do |method|
42
- define_method method do |*args, &block|
43
- Query.new collection_class,
44
- dataset.public_send(method, *args, &block),
45
- relations,
46
- schema
85
+ def each_batch(size:, &block)
86
+ primary_keys.each_slice(size) do |pks|
87
+ query = where(collection_class.primary_key => pks)
88
+ block.call query.all
47
89
  end
48
90
  end
49
91
 
50
- def graph(*rels)
51
- Query.new collection_class,
52
- dataset,
53
- (relations | rels),
54
- schema
92
+ def graph(*relations)
93
+ build_query relations_graph: relations_graph.merge(relations: relations)
55
94
  end
56
95
 
57
- def join(*rels)
58
- Query.new collection_class,
59
- Relations::GraphBuilder.joins_to(dataset, rels, collection_class, schema),
60
- relations,
61
- schema
96
+ def join(*relations)
97
+ graph = Relations::Graph.new environment, collection_class, relations
98
+
99
+ ds = graph.add_joins(dataset).distinct
100
+
101
+ build_query dataset: ds
62
102
  end
63
103
 
64
104
  def count
@@ -75,12 +115,12 @@ module Rasti
75
115
 
76
116
  def first
77
117
  row = dataset.first
78
- row ? collection_class.model.new(with_graph(row)) : nil
118
+ row ? build_model(row) : nil
79
119
  end
80
120
 
81
121
  def last
82
122
  row = dataset.last
83
- row ? collection_class.model.new(with_graph(row)) : nil
123
+ row ? build_model(row) : nil
84
124
  end
85
125
 
86
126
  def detect(*args, &block)
@@ -97,33 +137,63 @@ module Rasti
97
137
 
98
138
  raise NQL::InvalidExpressionError.new(filter_expression) if sentence.nil?
99
139
 
140
+ ds = sentence.computed_attributes(collection_class).inject(dataset) do |ds, name|
141
+ collection_class.computed_attributes[name].apply_join ds
142
+ end
143
+ query = build_query dataset: ds
144
+
100
145
  dependency_tables = sentence.dependency_tables
101
- query = dependency_tables.empty? ? self : join(*dependency_tables)
102
-
103
- query.where sentence.filter_condition
146
+ query = query.join(*dependency_tables) unless dependency_tables.empty?
147
+
148
+ query.where sentence.filter_condition(collection_class)
104
149
  end
105
150
 
106
151
  private
107
152
 
153
+ attr_reader :environment, :collection_class, :dataset, :relations_graph
154
+
155
+ def build_query(**args)
156
+ current_args = {
157
+ environment: environment,
158
+ collection_class: collection_class,
159
+ dataset: dataset,
160
+ relations_graph: relations_graph
161
+ }
162
+
163
+ Query.new(**current_args.merge(args))
164
+ end
165
+
166
+ def build_model(row)
167
+ collection_class.model.new with_graph(row)
168
+ end
169
+
108
170
  def chainable(&block)
109
- ds = instance_eval(&block)
110
- Query.new collection_class, ds, relations, schema
171
+ build_query dataset: instance_eval(&block)
111
172
  end
112
173
 
113
174
  def with_related(relation_name, primary_keys)
114
- ds = collection_class.relations[relation_name].apply_filter dataset, schema, primary_keys
115
- Query.new collection_class, ds, relations, schema
175
+ ds = collection_class.relations[relation_name].apply_filter environment, dataset, primary_keys
176
+ build_query dataset: ds
116
177
  end
117
178
 
118
179
  def with_graph(data)
119
180
  rows = data.is_a?(Array) ? data : [data]
120
- Relations::GraphBuilder.graph_to rows, relations, collection_class, dataset.db, schema
181
+ relations_graph.fetch_graph rows
121
182
  data
122
183
  end
123
184
 
185
+ def qualify(collection_name, data_source_name: nil)
186
+ data_source_name ||= collection_class.data_source_name
187
+ environment.qualify data_source_name, collection_name
188
+ end
189
+
190
+ def nql_parser
191
+ NQL::SyntaxParser.new
192
+ end
193
+
124
194
  def method_missing(method, *args, &block)
125
- if collection_class.queries.key?(method)
126
- instance_exec(*args, &collection_class.queries[method])
195
+ if collection_class.queries.key? method
196
+ instance_exec(*args, &collection_class.queries.fetch(method))
127
197
  else
128
198
  super
129
199
  end
@@ -133,12 +203,6 @@ module Rasti
133
203
  collection_class.queries.key?(method) || super
134
204
  end
135
205
 
136
- def nql_parser
137
- NQL::SyntaxParser.new
138
- end
139
-
140
- attr_reader :collection_class, :dataset, :relations, :schema
141
-
142
206
  end
143
207
  end
144
208
  end
@@ -33,25 +33,39 @@ module Rasti
33
33
  self.class == OneToOne
34
34
  end
35
35
 
36
- def join_relation_name(prefix)
37
- with_prefix prefix, name
36
+ def from_one?
37
+ one_to_one? || one_to_many?
38
38
  end
39
39
 
40
- private
40
+ def from_many?
41
+ many_to_one? || many_to_many?
42
+ end
41
43
 
42
- attr_reader :options
44
+ def to_one?
45
+ one_to_one? || many_to_one?
46
+ end
43
47
 
44
- def qualified_source_collection_name(schema=nil)
45
- schema.nil? ? Sequel[source_collection_class.collection_name] : Sequel[schema][source_collection_class.collection_name]
48
+ def to_many?
49
+ one_to_many? || many_to_many?
46
50
  end
47
51
 
48
- def qualified_target_collection_name(schema=nil)
49
- schema.nil? ? Sequel[target_collection_class.collection_name] : Sequel[schema][target_collection_class.collection_name]
52
+ def join_relation_name(prefix)
53
+ with_prefix prefix, name
50
54
  end
51
55
 
56
+ private
57
+
58
+ attr_reader :options
59
+
52
60
  def with_prefix(prefix, name)
53
61
  [prefix, name].compact.join('__').to_sym
54
62
  end
63
+
64
+ def validate_join!
65
+ if source_collection_class.data_source_name != target_collection_class.data_source_name
66
+ raise "Invalid join of multiple data sources: #{source_collection_class.data_source_name}.#{source_collection_class.collection_name} > #{target_collection_class.data_source_name}.#{target_collection_class.collection_name}"
67
+ end
68
+ end
55
69
 
56
70
  end
57
71
  end
@@ -0,0 +1,129 @@
1
+ module Rasti
2
+ module DB
3
+ module Relations
4
+ class Graph
5
+
6
+ def initialize(environment, collection_class, relations=[], selected_attributes={}, excluded_attributes={})
7
+ @environment = environment
8
+ @collection_class = collection_class
9
+ @graph = build_graph relations,
10
+ Hash::Indifferent.new(selected_attributes),
11
+ Hash::Indifferent.new(excluded_attributes)
12
+ end
13
+
14
+ def merge(relations:[], selected_attributes:{}, excluded_attributes:{})
15
+ Graph.new environment,
16
+ collection_class,
17
+ (flat_relations | relations),
18
+ flat_selected_attributes.merge(selected_attributes),
19
+ flat_excluded_attributes.merge(excluded_attributes)
20
+ end
21
+
22
+ def with_all_attributes_for(relations)
23
+ relations_with_all_attributes = relations.map { |r| [r, nil] }.to_h
24
+
25
+ merge selected_attributes: relations_with_all_attributes,
26
+ excluded_attributes: relations_with_all_attributes
27
+ end
28
+
29
+ def apply_to(query)
30
+ query.graph(*flat_relations)
31
+ .select_graph_attributes(flat_selected_attributes)
32
+ .exclude_graph_attributes(flat_excluded_attributes)
33
+ end
34
+
35
+ def fetch_graph(rows)
36
+ return if rows.empty?
37
+
38
+ graph.roots.each do |node|
39
+ relation_of(node).fetch_graph environment,
40
+ rows,
41
+ node[:selected_attributes],
42
+ node[:excluded_attributes] ,
43
+ subgraph_of(node)
44
+ end
45
+ end
46
+
47
+ def add_joins(dataset, prefix=nil)
48
+ graph.roots.inject(dataset) do |ds, node|
49
+ relation = relation_of node
50
+ dataset_with_relation = relation.add_join environment, ds, prefix
51
+ subgraph_of(node).add_joins dataset_with_relation, relation.join_relation_name(prefix)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :environment, :collection_class, :graph
58
+
59
+ def relation_of(node)
60
+ collection_class.relations.fetch(node[:name])
61
+ end
62
+
63
+ def flat_relations
64
+ graph.map(&:id)
65
+ end
66
+
67
+ def flat_selected_attributes
68
+ graph.each_with_object(Hash::Indifferent.new) do |node, hash|
69
+ hash[node.id] = node[:selected_attributes]
70
+ end
71
+ end
72
+
73
+ def flat_excluded_attributes
74
+ graph.each_with_object(Hash::Indifferent.new) do |node, hash|
75
+ hash[node.id] = node[:excluded_attributes]
76
+ end
77
+ end
78
+
79
+ def subgraph_of(node)
80
+ relations = []
81
+ selected = Hash::Indifferent.new
82
+ excluded = Hash::Indifferent.new
83
+
84
+ node.descendants.each do |descendant|
85
+ id = descendant.id[node[:name].length+1..-1]
86
+ relations << id
87
+ selected[id] = descendant[:selected_attributes]
88
+ excluded[id] = descendant[:excluded_attributes]
89
+ end
90
+
91
+ Graph.new environment,
92
+ relation_of(node).target_collection_class,
93
+ relations,
94
+ selected,
95
+ excluded
96
+ end
97
+
98
+ def build_graph(relations, selected_attributes, excluded_attributes)
99
+ HierarchicalGraph.new.tap do |graph|
100
+ flatten(relations).each do |relation|
101
+ sections = relation.split('.')
102
+
103
+ graph.add_node relation, name: sections.last.to_sym,
104
+ selected_attributes: selected_attributes[relation],
105
+ excluded_attributes: excluded_attributes[relation]
106
+
107
+ if sections.count > 1
108
+ parent_id = sections[0..-2].join('.')
109
+ graph.add_relation parent_id: parent_id,
110
+ child_id: relation
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def flatten(relations)
117
+ relations.flat_map do |relation|
118
+ parents = []
119
+ relation.to_s.split('.').map do |section|
120
+ parents << section
121
+ parents.compact.join('.')
122
+ end
123
+ end.uniq.sort
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+ end