rasti-db 1.3.0 → 2.0.1

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.
@@ -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