rom-sql 1.1.2 → 1.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.
- 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
|