rasti-db 1.4.0 → 2.2.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 (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