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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf05af538889499fb9fc1d4cb73ca842fae74482
4
- data.tar.gz: '07441291fd839e047161b70cb9f20c88ebb240f9'
3
+ metadata.gz: ef99832857b3fd2134904b5c5cd99d33ad1665d8
4
+ data.tar.gz: 5a08cac386afc1d218d51a19d5d2a2167cb617d7
5
5
  SHA512:
6
- metadata.gz: 447330e1514ca0aea9127434f02f2a81792e7702155f29c9a6c534938f1f88209ebc31328ef23cd7b368972854cb9e245916162ac05c981e59533c111c59ea54
7
- data.tar.gz: 35a04cc320ec2d246c58fddb5b95aeb3787af7f055f27395596d84cd56013572999648e514d9beeafc15b9f87d879cf29753f3cd3715a8df5e05fe44d2dc061a
6
+ metadata.gz: 712552863d0d9a5cb8c4ce14557ed8705bc274bddff9c9c3b613379a93f470aac04fa5eddce7d133ad1ffe9bd024d05bb08cd052babf7a158cf0da320cbf55d3
7
+ data.tar.gz: a0baf07fda4b9eb66fa0e296a39e2f1d7ed641db3fe3f9feaacf4d2ad8a2b587902c4f335115fa3edd662c5e0aca818bc3140528591a2eeb9b12b2e602bfa3a1
data/.codeclimate.yml ADDED
@@ -0,0 +1,15 @@
1
+ engines:
2
+ duplication:
3
+ enabled: true
4
+ config:
5
+ languages:
6
+ ruby:
7
+ mass_threshold: 25
8
+ rubocop:
9
+ enabled: true
10
+
11
+ ratings:
12
+ paths:
13
+ - "**.rb"
14
+
15
+ exclude_paths:
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, parent, assoc:, keys:)
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
- new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
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
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.1.2'.freeze
3
+ VERSION = '1.2.0'.freeze
4
4
  end
5
5
  end
@@ -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 { has_many :tasks }
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.1.2
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-02 00:00:00.000000000 Z
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.5.2
349
+ rubygems_version: 2.6.9
349
350
  signing_key:
350
351
  specification_version: 4
351
352
  summary: SQL databases support for ROM