rom-sql 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +15 -0
- data/CHANGELOG.md +10 -0
- data/lib/rom/sql/plugin/associates.rb +85 -87
- data/lib/rom/sql/relation/reading.rb +6 -1
- data/lib/rom/sql/version.rb +1 -1
- data/spec/integration/plugins/associates_spec.rb +15 -0
- data/spec/unit/relation/inner_join_spec.rb +57 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef99832857b3fd2134904b5c5cd99d33ad1665d8
|
4
|
+
data.tar.gz: 5a08cac386afc1d218d51a19d5d2a2167cb617d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 712552863d0d9a5cb8c4ce14557ed8705bc274bddff9c9c3b613379a93f470aac04fa5eddce7d133ad1ffe9bd024d05bb08cd052babf7a158cf0da320cbf55d3
|
7
|
+
data.tar.gz: a0baf07fda4b9eb66fa0e296a39e2f1d7ed641db3fe3f9feaacf4d2ad8a2b587902c4f335115fa3edd662c5e0aca818bc3140528591a2eeb9b12b2e602bfa3a1
|
data/.codeclimate.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## v1.2.0 2017-03-07
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Support for configuring multiple associations for a command (solnic)
|
6
|
+
* Support for passing parent tuple(s) as `parent` option in `Command#with_association` (solnic)
|
7
|
+
* Support for join using assocation name (flash-gordon)
|
8
|
+
|
9
|
+
[Compare v1.1.2...v1.2.0](https://github.com/rom-rb/rom-sql/compare/v1.1.2...v1.2.0)
|
10
|
+
|
1
11
|
## v1.1.2 2017-03-02
|
2
12
|
|
3
13
|
### Fixed
|
@@ -20,6 +20,34 @@ module ROM
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
class AssociateOptions
|
24
|
+
attr_reader :name, :assoc, :opts
|
25
|
+
|
26
|
+
def initialize(name, relation, opts)
|
27
|
+
@name = name
|
28
|
+
@opts = { assoc: name, keys: opts[:key] }
|
29
|
+
|
30
|
+
relation.associations.try(name) do |assoc|
|
31
|
+
@assoc = assoc
|
32
|
+
@opts.update(assoc: assoc, keys: assoc.join_keys(relation.__registry__))
|
33
|
+
end
|
34
|
+
|
35
|
+
@opts.update(parent: opts[:parent]) if opts[:parent]
|
36
|
+
end
|
37
|
+
|
38
|
+
def after?
|
39
|
+
assoc.is_a?(Association::ManyToMany)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ensure_valid(command)
|
43
|
+
raise MissingJoinKeysError.new(command, name) unless opts[:keys]
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_hash
|
47
|
+
{ associate: opts }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
23
51
|
# @api private
|
24
52
|
def self.included(klass)
|
25
53
|
klass.class_eval do
|
@@ -35,6 +63,62 @@ module ROM
|
|
35
63
|
super
|
36
64
|
end
|
37
65
|
|
66
|
+
module ClassMethods
|
67
|
+
# @see ROM::Command::ClassInterface.build
|
68
|
+
#
|
69
|
+
# @api public
|
70
|
+
def build(relation, options = EMPTY_HASH)
|
71
|
+
command = super
|
72
|
+
|
73
|
+
configured_assocs = command.configured_associations
|
74
|
+
|
75
|
+
associate_options = command.associations.map { |(name, opts)|
|
76
|
+
next if configured_assocs.include?(name)
|
77
|
+
AssociateOptions.new(name, relation, opts)
|
78
|
+
}.compact
|
79
|
+
|
80
|
+
associate_options.each { |opts| opts.ensure_valid(self) }
|
81
|
+
|
82
|
+
before_hooks = associate_options.reject(&:after?).map(&:to_hash)
|
83
|
+
after_hooks = associate_options.select(&:after?).map(&:to_hash)
|
84
|
+
|
85
|
+
command.
|
86
|
+
with_opts(configured_associations: configured_assocs + associate_options.map(&:name)).
|
87
|
+
before(*before_hooks).
|
88
|
+
after(*after_hooks)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Set command to associate tuples with a parent tuple using provided keys
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# class CreateTask < ROM::Commands::Create[:sql]
|
95
|
+
# relation :tasks
|
96
|
+
# associates :user, key: [:user_id, :id]
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# create_user = rom.command(:user).create.with(name: 'Jane')
|
100
|
+
#
|
101
|
+
# create_tasks = rom.command(:tasks).create
|
102
|
+
# .with [{ title: 'One' }, { title: 'Two' } ]
|
103
|
+
#
|
104
|
+
# command = create_user >> create_tasks
|
105
|
+
# command.call
|
106
|
+
#
|
107
|
+
# @param [Symbol] name The name of associated table
|
108
|
+
# @param [Hash] options The options
|
109
|
+
# @option options [Array] :key The association keys
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
def associates(name, options = EMPTY_HASH)
|
113
|
+
if associations.key?(name)
|
114
|
+
raise ArgumentError,
|
115
|
+
"#{name} association is already defined for #{self.class}"
|
116
|
+
end
|
117
|
+
|
118
|
+
associations(associations.merge(name => options))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
38
122
|
module InstanceMethods
|
39
123
|
# Set fk on tuples from parent tuple
|
40
124
|
#
|
@@ -44,7 +128,7 @@ module ROM
|
|
44
128
|
# @return [Array<Hash>]
|
45
129
|
#
|
46
130
|
# @api public
|
47
|
-
def associate(tuples,
|
131
|
+
def associate(tuples, curried_parent = nil, assoc:, keys:, parent: curried_parent)
|
48
132
|
result_type = result
|
49
133
|
|
50
134
|
output_tuples =
|
@@ -90,97 +174,11 @@ module ROM
|
|
90
174
|
)
|
91
175
|
end
|
92
176
|
|
93
|
-
def associations_configured?
|
94
|
-
if configured_associations.empty?
|
95
|
-
false
|
96
|
-
else
|
97
|
-
configured_associations.all? { |name| associations.key?(name) }
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
177
|
# @api private
|
102
178
|
def __registry__
|
103
179
|
relation.__registry__
|
104
180
|
end
|
105
181
|
end
|
106
|
-
|
107
|
-
module ClassMethods
|
108
|
-
# @see ROM::Command::ClassInterface.build
|
109
|
-
#
|
110
|
-
# @api public
|
111
|
-
def build(relation, options = EMPTY_HASH)
|
112
|
-
command = super
|
113
|
-
|
114
|
-
if command.associations_configured?
|
115
|
-
return command
|
116
|
-
end
|
117
|
-
|
118
|
-
associations = command.associations
|
119
|
-
assoc_names = []
|
120
|
-
|
121
|
-
before_hooks = associations.each_with_object([]) do |(name, opts), acc|
|
122
|
-
relation.associations.try(name) do |assoc|
|
123
|
-
unless assoc.is_a?(Association::ManyToMany)
|
124
|
-
acc << { associate: { assoc: assoc, keys: assoc.join_keys(relation.__registry__) } }
|
125
|
-
else
|
126
|
-
true
|
127
|
-
end
|
128
|
-
end or acc << { associate: { assoc: name, keys: opts[:key] } }
|
129
|
-
|
130
|
-
assoc_names << name
|
131
|
-
end
|
132
|
-
|
133
|
-
after_hooks = associations.each_with_object([]) do |(name, opts), acc|
|
134
|
-
next unless relation.associations.key?(name)
|
135
|
-
|
136
|
-
assoc = relation.associations[name]
|
137
|
-
|
138
|
-
if assoc.is_a?(Association::ManyToMany)
|
139
|
-
acc << { associate: { assoc: assoc, keys: assoc.join_keys(relation.__registry__) } }
|
140
|
-
assoc_names << name
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
[*before_hooks, *after_hooks].
|
145
|
-
map { |hook| hook[:associate] }.
|
146
|
-
each { |conf| raise MissingJoinKeysError.new(self, conf[:assoc]) unless conf[:keys] }
|
147
|
-
|
148
|
-
command.
|
149
|
-
with_opts(configured_associations: assoc_names).
|
150
|
-
before(*before_hooks).
|
151
|
-
after(*after_hooks)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Set command to associate tuples with a parent tuple using provided keys
|
155
|
-
#
|
156
|
-
# @example
|
157
|
-
# class CreateTask < ROM::Commands::Create[:sql]
|
158
|
-
# relation :tasks
|
159
|
-
# associates :user, key: [:user_id, :id]
|
160
|
-
# end
|
161
|
-
#
|
162
|
-
# create_user = rom.command(:user).create.with(name: 'Jane')
|
163
|
-
#
|
164
|
-
# create_tasks = rom.command(:tasks).create
|
165
|
-
# .with [{ title: 'One' }, { title: 'Two' } ]
|
166
|
-
#
|
167
|
-
# command = create_user >> create_tasks
|
168
|
-
# command.call
|
169
|
-
#
|
170
|
-
# @param [Symbol] name The name of associated table
|
171
|
-
# @param [Hash] options The options
|
172
|
-
# @option options [Array] :key The association keys
|
173
|
-
#
|
174
|
-
# @api public
|
175
|
-
def associates(name, options = EMPTY_HASH)
|
176
|
-
if associations.key?(name)
|
177
|
-
raise ArgumentError,
|
178
|
-
"#{name} association is already defined for #{self.class}"
|
179
|
-
end
|
180
|
-
|
181
|
-
associations(associations.merge(name => options))
|
182
|
-
end
|
183
|
-
end
|
184
182
|
end
|
185
183
|
end
|
186
184
|
end
|
@@ -811,7 +811,12 @@ module ROM
|
|
811
811
|
def __join__(type, other, join_cond = EMPTY_HASH, opts = EMPTY_HASH, &block)
|
812
812
|
case other
|
813
813
|
when Symbol, Association::Name
|
814
|
-
|
814
|
+
if join_cond.empty?
|
815
|
+
assoc = associations[other]
|
816
|
+
assoc.join(__registry__, type, self, __registry__[assoc.target.relation])
|
817
|
+
else
|
818
|
+
new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
|
819
|
+
end
|
815
820
|
when Relation
|
816
821
|
associations[other.name.dataset].join(__registry__, type, self, other)
|
817
822
|
else
|
data/lib/rom/sql/version.rb
CHANGED
@@ -38,6 +38,21 @@ RSpec.describe 'Plugins / :associates', seeds: false do
|
|
38
38
|
expect(command.call(task, user)).
|
39
39
|
to eql(id: 1, title: 'Task one', user_id: user[:id])
|
40
40
|
end
|
41
|
+
|
42
|
+
it 'allows passing a parent explicitly' do
|
43
|
+
command = tasks[:create].with_association(:user, key: %i[user_id id], parent: user)
|
44
|
+
|
45
|
+
expect(command.call(task)).
|
46
|
+
to eql(id: 1, title: 'Task one', user_id: user[:id])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'allows setting up multiple associations' do
|
50
|
+
command = tasks[:create].
|
51
|
+
with_association(:user, key: %i[user_id id], parent: user).
|
52
|
+
with_association(:other, key: %i[other_id id])
|
53
|
+
|
54
|
+
expect(command.configured_associations).to eql(%i[user other])
|
55
|
+
end
|
41
56
|
end
|
42
57
|
|
43
58
|
shared_context 'automatic FK setting' do
|
@@ -3,6 +3,7 @@ RSpec.describe ROM::Relation, '#inner_join' do
|
|
3
3
|
|
4
4
|
let(:tasks) { relations[:tasks] }
|
5
5
|
let(:tags) { relations[:tags] }
|
6
|
+
let(:puzzles) { relations[:puzzles] }
|
6
7
|
|
7
8
|
include_context 'users and tasks'
|
8
9
|
|
@@ -39,9 +40,22 @@ RSpec.describe ROM::Relation, '#inner_join' do
|
|
39
40
|
|
40
41
|
context 'with associations' do
|
41
42
|
before do
|
43
|
+
inferrable_relations.concat %i(puzzles)
|
44
|
+
end
|
45
|
+
|
46
|
+
before do
|
47
|
+
conn.create_table(:puzzles) do
|
48
|
+
primary_key :id
|
49
|
+
foreign_key :author_id, :users, null: false
|
50
|
+
column :text, String, null: false
|
51
|
+
end
|
52
|
+
|
42
53
|
conf.relation(:users) do
|
43
54
|
schema(infer: true) do
|
44
|
-
associations
|
55
|
+
associations do
|
56
|
+
has_many :tasks
|
57
|
+
has_many :tasks, as: :todos, relation: :tasks
|
58
|
+
end
|
45
59
|
end
|
46
60
|
end
|
47
61
|
|
@@ -64,7 +78,16 @@ RSpec.describe ROM::Relation, '#inner_join' do
|
|
64
78
|
end
|
65
79
|
end
|
66
80
|
|
81
|
+
conf.relation(:puzzles) do
|
82
|
+
schema(infer: true) do
|
83
|
+
associations do
|
84
|
+
belongs_to :users, as: :author
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
67
89
|
relation.insert id: 3, name: 'Jade'
|
90
|
+
puzzles.insert id: 1, author_id: 1, text: 'solved by Jane'
|
68
91
|
end
|
69
92
|
|
70
93
|
it 'joins relation with join keys inferred' do
|
@@ -85,6 +108,39 @@ RSpec.describe ROM::Relation, '#inner_join' do
|
|
85
108
|
|
86
109
|
expect(result.to_a).to eql([{ id: 1, user_id: 2, title: "Joe's task" }])
|
87
110
|
end
|
111
|
+
|
112
|
+
it 'joins by association name if no condition provided' do
|
113
|
+
result = relation.
|
114
|
+
inner_join(:tasks).
|
115
|
+
select(:name, tasks[:title])
|
116
|
+
|
117
|
+
expect(result.schema.map(&:name)).to eql(%i[name title])
|
118
|
+
|
119
|
+
expect(result.to_a).to eql([
|
120
|
+
{ name: 'Jane', title: "Jane's task" },
|
121
|
+
{ name: 'Joe', title: "Joe's task" }
|
122
|
+
])
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'joins if association name differs from relation name' do
|
126
|
+
result = relation.
|
127
|
+
inner_join(:todos).
|
128
|
+
select(:name, tasks[:title])
|
129
|
+
|
130
|
+
expect(result.schema.map(&:name)).to eql(%i[name title])
|
131
|
+
|
132
|
+
expect(result.to_a).to eql([
|
133
|
+
{ name: 'Jane', title: "Jane's task" },
|
134
|
+
{ name: 'Joe', title: "Joe's task" }
|
135
|
+
])
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'joins by relation if association name differs from relation name' do
|
139
|
+
pending 'waits for support for joins by aliased relation'
|
140
|
+
result = puzzles.inner_join(users).select(:name, puzzles[:text])
|
141
|
+
|
142
|
+
expect(result.to_a).to eql([ name: 'Jane', title: "Jane's task" ])
|
143
|
+
end
|
88
144
|
end
|
89
145
|
|
90
146
|
it 'raises error when column names are ambiguous' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rom-sql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -141,6 +141,7 @@ executables: []
|
|
141
141
|
extensions: []
|
142
142
|
extra_rdoc_files: []
|
143
143
|
files:
|
144
|
+
- ".codeclimate.yml"
|
144
145
|
- ".gitignore"
|
145
146
|
- ".rspec"
|
146
147
|
- ".travis.yml"
|
@@ -345,7 +346,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
345
346
|
version: '0'
|
346
347
|
requirements: []
|
347
348
|
rubyforge_project:
|
348
|
-
rubygems_version: 2.
|
349
|
+
rubygems_version: 2.6.9
|
349
350
|
signing_key:
|
350
351
|
specification_version: 4
|
351
352
|
summary: SQL databases support for ROM
|