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.
- checksums.yaml +4 -4
- data/.travis.yml +7 -12
- data/README.md +77 -21
- data/lib/rasti/db.rb +2 -1
- data/lib/rasti/db/collection.rb +70 -46
- data/lib/rasti/db/data_source.rb +18 -0
- data/lib/rasti/db/environment.rb +32 -0
- data/lib/rasti/db/model.rb +4 -0
- data/lib/rasti/db/query.rb +71 -37
- data/lib/rasti/db/relations/base.rb +22 -8
- data/lib/rasti/db/relations/graph.rb +129 -0
- data/lib/rasti/db/relations/many_to_many.rb +59 -25
- data/lib/rasti/db/relations/many_to_one.rb +17 -12
- data/lib/rasti/db/relations/one_to_many.rb +27 -16
- data/lib/rasti/db/version.rb +1 -1
- data/rasti-db.gemspec +3 -8
- data/spec/collection_spec.rb +223 -52
- data/spec/minitest_helper.rb +50 -14
- data/spec/model_spec.rb +9 -1
- data/spec/query_spec.rb +199 -77
- data/spec/relations_spec.rb +27 -7
- metadata +25 -10
- data/lib/rasti/db/helpers.rb +0 -16
- data/lib/rasti/db/relations/graph_builder.rb +0 -60
@@ -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
|
data/lib/rasti/db/model.rb
CHANGED
data/lib/rasti/db/query.rb
CHANGED
@@ -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
|
9
|
+
def initialize(environment:, collection_class:, dataset:, relations_graph:nil)
|
10
|
+
@environment = environment
|
11
11
|
@collection_class = collection_class
|
12
|
-
@dataset = dataset
|
13
|
-
@
|
14
|
-
|
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 { |
|
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
|
-
|
42
|
-
|
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
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
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
|
115
|
-
|
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
|
-
|
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?
|
126
|
-
instance_exec(*args, &collection_class.queries
|
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
|
37
|
-
|
36
|
+
def from_one?
|
37
|
+
one_to_one? || one_to_many?
|
38
38
|
end
|
39
39
|
|
40
|
-
|
40
|
+
def from_many?
|
41
|
+
many_to_one? || many_to_many?
|
42
|
+
end
|
41
43
|
|
42
|
-
|
44
|
+
def to_one?
|
45
|
+
one_to_one? || many_to_one?
|
46
|
+
end
|
43
47
|
|
44
|
-
def
|
45
|
-
|
48
|
+
def to_many?
|
49
|
+
one_to_many? || many_to_many?
|
46
50
|
end
|
47
51
|
|
48
|
-
def
|
49
|
-
|
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
|
19
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
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[
|
63
|
+
row[name] = relation_rows.fetch row[source_collection_class.primary_key], []
|
45
64
|
end
|
46
65
|
end
|
47
66
|
|
48
|
-
def
|
49
|
-
|
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
|
-
|
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] =>
|
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(
|
64
|
-
.join(
|
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
|
-
|
68
|
-
relation_name = qualified_relation_collection_name schema
|
104
|
+
private
|
69
105
|
|
70
|
-
|
71
|
-
|
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
|