rasti-db 1.3.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module Rasti
2
+ module DB
3
+ class DataSource
4
+
5
+ attr_reader :db, :schema
6
+
7
+ def initialize(db, schema=nil)
8
+ @db = db
9
+ @schema = schema ? schema.to_sym : nil
10
+ end
11
+
12
+ def qualify(collection_name)
13
+ schema ? Sequel[schema][collection_name] : Sequel[collection_name]
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module Rasti
2
+ module DB
3
+ class Environment
4
+
5
+ def initialize(data_sources)
6
+ @data_sources = data_sources
7
+ end
8
+
9
+ def data_source(name)
10
+ raise "Undefined data source #{name}" unless data_sources.key? name
11
+ data_sources[name]
12
+ end
13
+
14
+ def data_source_of(collection_class)
15
+ data_source collection_class.data_source_name
16
+ end
17
+
18
+ def qualify(data_source_name, collection_name)
19
+ data_source(data_source_name).qualify collection_name
20
+ end
21
+
22
+ def qualify_collection(collection_class)
23
+ data_source_of(collection_class).qualify collection_class.collection_name
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :data_sources
29
+
30
+ end
31
+ end
32
+ end
@@ -62,6 +62,10 @@ module Rasti
62
62
  @attributes = attributes
63
63
  end
64
64
 
65
+ def merge(new_attributes)
66
+ self.class.new attributes.merge(new_attributes)
67
+ end
68
+
65
69
  def eql?(other)
66
70
  instance_of?(other.class) && to_h.eql?(other.to_h)
67
71
  end
@@ -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,6 +32,31 @@ 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
+
30
60
  def all
31
61
  with_graph(dataset.all).map do |row|
32
62
  collection_class.model.new row
@@ -38,27 +68,16 @@ module Rasti
38
68
  all.each(&block)
39
69
  end
40
70
 
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
47
- end
71
+ def graph(*relations)
72
+ build_query relations_graph: relations_graph.merge(relations: relations)
48
73
  end
49
74
 
50
- def graph(*rels)
51
- Query.new collection_class,
52
- dataset,
53
- (relations | rels),
54
- schema
55
- end
75
+ def join(*relations)
76
+ graph = Relations::Graph.new environment, collection_class, relations
77
+
78
+ ds = graph.add_joins(dataset).distinct
56
79
 
57
- def join(*rels)
58
- Query.new collection_class,
59
- Relations::GraphBuilder.joins_to(dataset, rels, collection_class, schema),
60
- relations,
61
- schema
80
+ build_query dataset: ds
62
81
  end
63
82
 
64
83
  def count
@@ -105,25 +124,46 @@ module Rasti
105
124
 
106
125
  private
107
126
 
127
+ attr_reader :environment, :collection_class, :dataset, :relations_graph
128
+
129
+ def build_query(**args)
130
+ current_args = {
131
+ environment: environment,
132
+ collection_class: collection_class,
133
+ dataset: dataset,
134
+ relations_graph: relations_graph
135
+ }
136
+
137
+ Query.new(**current_args.merge(args))
138
+ end
139
+
108
140
  def chainable(&block)
109
- ds = instance_eval(&block)
110
- Query.new collection_class, ds, relations, schema
141
+ build_query dataset: instance_eval(&block)
111
142
  end
112
143
 
113
144
  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
145
+ ds = collection_class.relations[relation_name].apply_filter environment, dataset, primary_keys
146
+ build_query dataset: ds
116
147
  end
117
148
 
118
149
  def with_graph(data)
119
150
  rows = data.is_a?(Array) ? data : [data]
120
- Relations::GraphBuilder.graph_to rows, relations, collection_class, dataset.db, schema
151
+ relations_graph.fetch_graph rows
121
152
  data
122
153
  end
123
154
 
155
+ def qualify(collection_name, data_source_name: nil)
156
+ data_source_name ||= collection_class.data_source_name
157
+ environment.qualify data_source_name, collection_name
158
+ end
159
+
160
+ def nql_parser
161
+ NQL::SyntaxParser.new
162
+ end
163
+
124
164
  def method_missing(method, *args, &block)
125
- if collection_class.queries.key?(method)
126
- instance_exec(*args, &collection_class.queries[method])
165
+ if collection_class.queries.key? method
166
+ instance_exec(*args, &collection_class.queries.fetch(method))
127
167
  else
128
168
  super
129
169
  end
@@ -133,12 +173,6 @@ module Rasti
133
173
  collection_class.queries.key?(method) || super
134
174
  end
135
175
 
136
- def nql_parser
137
- NQL::SyntaxParser.new
138
- end
139
-
140
- attr_reader :collection_class, :dataset, :relations, :schema
141
-
142
176
  end
143
177
  end
144
178
  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
@@ -15,25 +15,44 @@ module Rasti
15
15
  @relation_collection_name ||= options[:relation_collection_name] || [source_collection_class.collection_name, target_collection_class.collection_name].sort.join('_').to_sym
16
16
  end
17
17
 
18
- def qualified_relation_collection_name(schema=nil)
19
- schema.nil? ? Sequel[relation_collection_name] : Sequel[schema][relation_collection_name]
18
+ def relation_data_source_name
19
+ @relation_data_source_name ||= options[:relation_data_source_name] || source_collection_class.data_source_name
20
20
  end
21
21
 
22
- def graph_to(rows, db, schema=nil, relations=[])
22
+ def fetch_graph(environment, rows, selected_attributes=nil, excluded_attributes=nil, relations_graph=nil)
23
23
  pks = rows.map { |row| row[source_collection_class.primary_key] }
24
24
 
25
- target_collection = target_collection_class.new db, schema
25
+ if target_collection_class.data_source_name == relation_data_source_name
26
+ target_data_source = environment.data_source_of target_collection_class
26
27
 
27
- relation_name = qualified_relation_collection_name schema
28
+ dataset = target_data_source.db.from(environment.qualify_collection(target_collection_class))
29
+ .join(qualified_relation_collection_name(environment), target_foreign_key => target_collection_class.primary_key)
30
+ .where(Sequel[relation_collection_name][source_foreign_key] => pks)
31
+ .select_all(target_collection_class.collection_name)
28
32
 
29
- join_rows = target_collection.dataset
30
- .join(relation_name, target_foreign_key => target_collection_class.primary_key)
31
- .where(Sequel[relation_name][source_foreign_key] => pks)
32
- .select_all(qualified_target_collection_name(schema))
33
- .select_append(Sequel[relation_name][source_foreign_key].as(:source_foreign_key))
34
- .all
33
+ selected_attributes ||= target_collection_class.collection_attributes - excluded_attributes if excluded_attributes
34
+ dataset = dataset.select(*selected_attributes.map { |a| Sequel[target_collection_class.collection_name][a] }) if selected_attributes
35
35
 
36
- GraphBuilder.graph_to join_rows, relations, target_collection_class, db, schema
36
+ join_rows = dataset.select_append(Sequel[relation_collection_name][source_foreign_key].as(:source_foreign_key)).all
37
+ else
38
+ relation_data_source = environment.data_source relation_data_source_name
39
+
40
+ relation_index = relation_data_source.db.from(relation_data_source.qualify(relation_collection_name))
41
+ .where(source_foreign_key => pks)
42
+ .select_hash_groups(target_foreign_key, source_foreign_key)
43
+
44
+ query = target_collection_class.new environment
45
+ query = query.exclude_attributes(*excluded_attributes) if excluded_attributes
46
+ query = query.select_attributes(*selected_attributes) if selected_attributes
47
+
48
+ join_rows = query.where(target_collection_class.primary_key => relation_index.keys).raw.flat_map do |row|
49
+ relation_index[row[target_collection_class.primary_key]].map do |source_primary_key|
50
+ row.merge(source_foreign_key: source_primary_key)
51
+ end
52
+ end
53
+ end
54
+
55
+ relations_graph.fetch_graph join_rows if relations_graph
37
56
 
38
57
  relation_rows = join_rows.each_with_object(Hash.new { |h,k| h[k] = [] }) do |row, hash|
39
58
  attributes = row.select { |attr,_| target_collection_class.model.attributes.include? attr }
@@ -41,17 +60,19 @@ module Rasti
41
60
  end
42
61
 
43
62
  rows.each do |row|
44
- row[name] = relation_rows.fetch row[target_collection_class.primary_key], []
63
+ row[name] = relation_rows.fetch row[source_collection_class.primary_key], []
45
64
  end
46
65
  end
47
66
 
48
- def join_to(dataset, schema=nil, prefix=nil)
49
- many_to_many_relation_alias = with_prefix prefix, relation_collection_name
67
+ def add_join(environment, dataset, prefix=nil)
68
+ validate_join!
69
+
70
+ many_to_many_relation_alias = with_prefix prefix, "#{relation_collection_name}_#{SecureRandom.base64}"
50
71
 
51
- qualified_relation_source = prefix ? Sequel[prefix] : qualified_source_collection_name(schema)
72
+ relation_name = prefix ? Sequel[prefix] : Sequel[source_collection_class.collection_name]
52
73
 
53
74
  many_to_many_condition = {
54
- Sequel[many_to_many_relation_alias][source_foreign_key] => qualified_relation_source[source_collection_class.primary_key]
75
+ Sequel[many_to_many_relation_alias][source_foreign_key] => relation_name[source_collection_class.primary_key]
55
76
  }
56
77
 
57
78
  relation_alias = join_relation_name prefix
@@ -60,17 +81,30 @@ module Rasti
60
81
  Sequel[relation_alias][target_collection_class.primary_key] => Sequel[many_to_many_relation_alias][target_foreign_key]
61
82
  }
62
83
 
63
- dataset.join(qualified_relation_collection_name(schema).as(many_to_many_relation_alias), many_to_many_condition)
64
- .join(qualified_target_collection_name(schema).as(relation_alias), relation_condition)
84
+ dataset.join(qualified_relation_collection_name(environment).as(many_to_many_relation_alias), many_to_many_condition)
85
+ .join(environment.qualify_collection(target_collection_class).as(relation_alias), relation_condition)
86
+ end
87
+
88
+ def apply_filter(environment, dataset, primary_keys)
89
+ if source_collection_class.data_source_name == relation_data_source_name
90
+ dataset.join(qualified_relation_collection_name(environment), source_foreign_key => source_collection_class.primary_key)
91
+ .where(Sequel[relation_collection_name][target_foreign_key] => primary_keys)
92
+ .select_all(source_collection_class.collection_name)
93
+ .distinct
94
+ else
95
+ data_source = environment.data_source relation_data_source_name
96
+ fks = data_source.db.from(data_source.qualify(relation_collection_name))
97
+ .where(target_collection_class.foreign_key => primary_keys)
98
+ .select_map(source_collection_class.foreign_key)
99
+ .uniq
100
+ dataset.where(source_collection_class.primary_key => fks)
101
+ end
65
102
  end
66
103
 
67
- def apply_filter(dataset, schema=nil, primary_keys=[])
68
- relation_name = qualified_relation_collection_name schema
104
+ private
69
105
 
70
- dataset.join(relation_name, source_foreign_key => target_collection_class.primary_key)
71
- .where(Sequel[relation_name][target_foreign_key] => primary_keys)
72
- .select_all(qualified_source_collection_name(schema))
73
- .distinct
106
+ def qualified_relation_collection_name(environment)
107
+ environment.qualify relation_data_source_name, relation_collection_name
74
108
  end
75
109
 
76
110
  end