rom-repository 1.4.0 → 2.0.0.beta1

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -6
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +18 -1
  5. data/lib/rom-repository.rb +1 -2
  6. data/lib/rom/repository.rb +9 -216
  7. data/lib/rom/repository/class_interface.rb +16 -33
  8. data/lib/rom/repository/relation_reader.rb +46 -0
  9. data/lib/rom/repository/root.rb +3 -59
  10. data/lib/rom/repository/version.rb +1 -1
  11. metadata +9 -98
  12. data/.gitignore +0 -3
  13. data/.rspec +0 -3
  14. data/.travis.yml +0 -27
  15. data/.yardopts +0 -2
  16. data/Gemfile +0 -38
  17. data/Rakefile +0 -19
  18. data/lib/rom/open_struct.rb +0 -35
  19. data/lib/rom/repository/changeset.rb +0 -155
  20. data/lib/rom/repository/changeset/associated.rb +0 -100
  21. data/lib/rom/repository/changeset/create.rb +0 -16
  22. data/lib/rom/repository/changeset/delete.rb +0 -17
  23. data/lib/rom/repository/changeset/pipe.rb +0 -97
  24. data/lib/rom/repository/changeset/restricted.rb +0 -28
  25. data/lib/rom/repository/changeset/stateful.rb +0 -282
  26. data/lib/rom/repository/changeset/update.rb +0 -82
  27. data/lib/rom/repository/command_compiler.rb +0 -257
  28. data/lib/rom/repository/command_proxy.rb +0 -26
  29. data/lib/rom/repository/header_builder.rb +0 -65
  30. data/lib/rom/repository/mapper_builder.rb +0 -23
  31. data/lib/rom/repository/relation_proxy.rb +0 -337
  32. data/lib/rom/repository/relation_proxy/combine.rb +0 -320
  33. data/lib/rom/repository/relation_proxy/wrap.rb +0 -78
  34. data/lib/rom/repository/struct_builder.rb +0 -83
  35. data/lib/rom/struct.rb +0 -113
  36. data/log/.gitkeep +0 -0
  37. data/rom-repository.gemspec +0 -23
  38. data/spec/integration/changeset_spec.rb +0 -193
  39. data/spec/integration/command_macros_spec.rb +0 -191
  40. data/spec/integration/command_spec.rb +0 -228
  41. data/spec/integration/multi_adapter_spec.rb +0 -73
  42. data/spec/integration/repository/aggregate_spec.rb +0 -58
  43. data/spec/integration/repository_spec.rb +0 -406
  44. data/spec/integration/root_repository_spec.rb +0 -106
  45. data/spec/integration/typed_structs_spec.rb +0 -64
  46. data/spec/shared/database.rb +0 -79
  47. data/spec/shared/mappers.rb +0 -35
  48. data/spec/shared/models.rb +0 -41
  49. data/spec/shared/plugins.rb +0 -66
  50. data/spec/shared/relations.rb +0 -115
  51. data/spec/shared/repo.rb +0 -86
  52. data/spec/shared/seeds.rb +0 -30
  53. data/spec/shared/structs.rb +0 -140
  54. data/spec/spec_helper.rb +0 -83
  55. data/spec/support/mapper_registry.rb +0 -9
  56. data/spec/support/mutant.rb +0 -10
  57. data/spec/unit/changeset/associate_spec.rb +0 -120
  58. data/spec/unit/changeset/map_spec.rb +0 -111
  59. data/spec/unit/changeset_spec.rb +0 -186
  60. data/spec/unit/relation_proxy_spec.rb +0 -202
  61. data/spec/unit/repository/changeset_spec.rb +0 -197
  62. data/spec/unit/repository/inspect_spec.rb +0 -18
  63. data/spec/unit/repository/session_spec.rb +0 -251
  64. data/spec/unit/repository/transaction_spec.rb +0 -42
  65. data/spec/unit/session_spec.rb +0 -46
  66. data/spec/unit/struct_builder_spec.rb +0 -128
@@ -1,337 +0,0 @@
1
- require 'dry/core/deprecations'
2
-
3
- require 'rom/initializer'
4
- require 'rom/relation/materializable'
5
-
6
- require 'rom/repository/relation_proxy/combine'
7
- require 'rom/repository/relation_proxy/wrap'
8
-
9
- module ROM
10
- class Repository
11
- # RelationProxy decorates a relation and automatically generates mappers that
12
- # will map raw tuples into rom structs
13
- #
14
- # Relation proxies are being registered within repositories so typically there's
15
- # no need to instantiate them manually.
16
- #
17
- # @api public
18
- class RelationProxy
19
- extend Initializer
20
- extend Dry::Core::Deprecations[:rom]
21
-
22
- include Relation::Materializable
23
-
24
- (Kernel.private_instance_methods - %i(raise)).each(&method(:undef_method))
25
-
26
- include RelationProxy::Combine
27
- include RelationProxy::Wrap
28
-
29
- deprecate :combine_parents
30
- deprecate :combine_children
31
- deprecate :wrap_parent
32
-
33
- RelationRegistryType = Types.Definition(RelationRegistry).constrained(type: RelationRegistry)
34
-
35
- # @!attribute [r] relation
36
- # @return [Relation, Relation::Composite, Relation::Graph, Relation::Curried] The decorated relation object
37
- param :relation
38
-
39
- option :name, type: Types::Strict::Symbol
40
- option :mappers, default: -> { MapperBuilder.new }
41
- option :meta, default: -> { EMPTY_HASH }
42
- option :registry, type: RelationRegistryType, default: -> { RelationRegistry.new }
43
- option :auto_struct, default: -> { true }
44
-
45
- # Relation name
46
- #
47
- # @return [ROM::Relation::Name]
48
- #
49
- # @api public
50
- def name
51
- @name ? relation.name.with(@name) : relation.name
52
- end
53
-
54
- # Materializes wrapped relation and sends it through a mapper
55
- #
56
- # For performance reasons a combined relation will skip mapping since
57
- # we only care about extracting key values for combining
58
- #
59
- # @api public
60
- def call(*args)
61
- ((combine? || composite?) ? relation : (relation >> mapper)).call(*args)
62
- end
63
-
64
- # Maps the wrapped relation with other mappers available in the registry
65
- #
66
- # @overload map_with(model)
67
- # Map tuples to the provided custom model class
68
- #
69
- # @example
70
- # users.as(MyUserModel)
71
- #
72
- # @param [Class>] model Your custom model class
73
- #
74
- # @overload map_with(*mappers)
75
- # Map tuples using registered mappers
76
- #
77
- # @example
78
- # users.map_with(:my_mapper, :my_other_mapper)
79
- #
80
- # @param [Array<Symbol>] mappers A list of mapper identifiers
81
- #
82
- # @overload map_with(*mappers, auto_map: true)
83
- # Map tuples using auto-mapping and custom registered mappers
84
- #
85
- # If `auto_map` is enabled, your mappers will be applied after performing
86
- # default auto-mapping. This means that you can compose complex relations
87
- # and have them auto-mapped, and use much simpler custom mappers to adjust
88
- # resulting data according to your requirements.
89
- #
90
- # @example
91
- # users.map_with(:my_mapper, :my_other_mapper, auto_map: true)
92
- #
93
- # @param [Array<Symbol>] mappers A list of mapper identifiers
94
- #
95
- # @return [RelationProxy] A new relation proxy with pipelined relation
96
- #
97
- # @api public
98
- def map_with(*names, **opts)
99
- if names.size == 1 && names[0].is_a?(Class)
100
- map_to(names[0])
101
- elsif names.size > 1 && names.any? { |name| name.is_a?(Class) }
102
- raise ArgumentError, 'using custom mappers and a model is not supported'
103
- else
104
- if opts[:auto_map] && !meta[:combine_type]
105
- mappers = [mapper, *names.map { |name| relation.mappers[name] }]
106
- mappers.reduce(self) { |a, e| a >> e }
107
- else
108
- names.reduce(self) { |a, e| a >> relation.mappers[e] }
109
- end
110
- end
111
- end
112
-
113
- # @api public
114
- def as(*names, **opts)
115
- if names.size == 1 && names[0].is_a?(Class)
116
- msg = <<-STR
117
- Relation#as will change behavior in 4.0. Use `map_to` instead
118
- => Called at:
119
- #{Kernel.caller[0..5].join("\n")}
120
- STR
121
-
122
- Dry::Core::Deprecations.warn(msg)
123
-
124
- map_to(names[0])
125
- elsif names.size > 1 && names.any? { |name| name.is_a?(Class) }
126
- raise ArgumentError, 'using custom mappers and a model is not supported'
127
- else
128
- if opts[:auto_map] && !meta[:combine_type]
129
- mappers = [mapper, *names.map { |name| relation.mappers[name] }]
130
- mappers.reduce(self) { |a, e| a >> e }
131
- else
132
- names.reduce(self) { |a, e| a >> relation.mappers[e] }
133
- end
134
- end
135
- end
136
-
137
- # @api public
138
- def map_to(model)
139
- with(meta: meta.merge(model: model))
140
- end
141
-
142
- # Return a new graph with adjusted node returned from a block
143
- #
144
- # @example with a node identifier
145
- # aggregate(:tasks).node(:tasks) { |tasks| tasks.prioritized }
146
- #
147
- # @example with a nested path
148
- # aggregate(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
149
- #
150
- # @param [Symbol] name The node relation name
151
- #
152
- # @yieldparam [RelationProxy] The relation node
153
- # @yieldreturn [RelationProxy] The new relation node
154
- #
155
- # @return [RelationProxy]
156
- #
157
- # @api public
158
- def node(name, &block)
159
- if name.is_a?(Symbol) && !nodes.map { |n| n.name.relation }.include?(name)
160
- raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
161
- end
162
-
163
- new_nodes = nodes.map { |node|
164
- case name
165
- when Symbol
166
- name == node.name.relation ? yield(node) : node
167
- when Hash
168
- other, *rest = name.flatten(1)
169
- if other == node.name.relation
170
- nodes.detect { |n| n.name.relation == other }.node(*rest, &block)
171
- else
172
- node
173
- end
174
- else
175
- node
176
- end
177
- }
178
-
179
- with_nodes(new_nodes)
180
- end
181
-
182
- # Return a string representation of this relation proxy
183
- #
184
- # @return [String]
185
- #
186
- # @api public
187
- def inspect
188
- %(#<#{relation.class} name=#{name} dataset=#{dataset.inspect}>)
189
- end
190
-
191
- # Infers a mapper for the wrapped relation
192
- #
193
- # @return [ROM::Mapper]
194
- #
195
- # @api private
196
- def mapper
197
- mappers[to_ast]
198
- end
199
-
200
- # Returns a new instance with new options
201
- #
202
- # @param new_options [Hash]
203
- #
204
- # @return [RelationProxy]
205
- #
206
- # @api private
207
- def with(new_options)
208
- __new__(relation, options.merge(new_options))
209
- end
210
-
211
- # Returns if this relation is combined aka a relation graph
212
- #
213
- # @return [Boolean]
214
- #
215
- # @api private
216
- def combine?
217
- meta[:combine_type]
218
- end
219
-
220
- # Return if this relation is a composite
221
- #
222
- # @return [Boolean]
223
- #
224
- # @api private
225
- def composite?
226
- relation.is_a?(Relation::Composite)
227
- end
228
-
229
- # @return [Symbol] The wrapped relation's adapter identifier ie :sql or :http
230
- #
231
- # @api private
232
- def adapter
233
- relation.class.adapter
234
- end
235
-
236
- # Returns AST for the wrapped relation
237
- #
238
- # @return [Array]
239
- #
240
- # @api private
241
- def to_ast
242
- @to_ast ||=
243
- begin
244
- attr_ast = schema.map { |attr| [:attribute, attr] }
245
-
246
- meta = self.meta.merge(dataset: base_name.dataset)
247
- meta.update(model: false) unless meta[:model] || auto_struct
248
- meta.delete(:wraps)
249
-
250
- header = attr_ast + nodes_ast + wraps_ast
251
-
252
- [:relation, [base_name.relation, meta, [:header, header]]]
253
- end
254
- end
255
-
256
- # @api private
257
- def respond_to_missing?(meth, _include_private = false)
258
- relation.respond_to?(meth) || super
259
- end
260
-
261
- private
262
-
263
- # @api private
264
- def schema
265
- if meta[:wrap]
266
- relation.schema.wrap
267
- else
268
- relation.schema.reject(&:wrapped?)
269
- end
270
- end
271
-
272
- # @api private
273
- def base_name
274
- relation.base_name
275
- end
276
-
277
- # @api private
278
- def nodes_ast
279
- @nodes_ast ||= nodes.map(&:to_ast)
280
- end
281
-
282
- # @api private
283
- def wraps_ast
284
- @wraps_ast ||= wraps.map(&:to_ast)
285
- end
286
-
287
- # Return a new instance with another relation and options
288
- #
289
- # @return [RelationProxy]
290
- #
291
- # @api private
292
- def __new__(relation, new_options = EMPTY_HASH)
293
- self.class.new(
294
- relation, new_options.size > 0 ? options.merge(new_options) : options
295
- )
296
- end
297
-
298
- # Return all nodes that this relation combines
299
- #
300
- # @return [Array<RelationProxy>]
301
- #
302
- # @api private
303
- def nodes
304
- relation.graph? ? relation.nodes : EMPTY_ARRAY
305
- end
306
-
307
- # Return all nodes that this relation wraps
308
- #
309
- # @return [Array<RelationProxy>]
310
- #
311
- # @api private
312
- def wraps
313
- meta.fetch(:wraps, EMPTY_ARRAY)
314
- end
315
-
316
- # Forward to relation and wrap it with proxy if response was a relation too
317
- #
318
- # TODO: this will be simplified once ROM::Relation has lazy-features built-in
319
- # and ROM::Lazy is gone
320
- #
321
- # @api private
322
- def method_missing(meth, *args, &block)
323
- if relation.respond_to?(meth)
324
- result = relation.__send__(meth, *args, &block)
325
-
326
- if result.kind_of?(Relation::Materializable) && !result.is_a?(Relation::Loaded)
327
- __new__(result)
328
- else
329
- result
330
- end
331
- else
332
- raise NoMethodError, "undefined method `#{meth}' for #{relation.class.name}"
333
- end
334
- end
335
- end
336
- end
337
- end
@@ -1,320 +0,0 @@
1
- require 'dry/core/inflector'
2
-
3
- module ROM
4
- class Repository
5
- class RelationProxy
6
- # Provides convenient methods for composing relations
7
- #
8
- # @api public
9
- module Combine
10
- # Returns a combine representation of a loading-proxy relation
11
- #
12
- # This will carry meta info used to produce a correct AST from a relation
13
- # so that correct mapper can be generated
14
- #
15
- # @return [RelationProxy]
16
- #
17
- # @api private
18
- def combined(name, keys, type)
19
- meta = { keys: keys, combine_type: type, combine_name: name }
20
- with(name: name, meta: self.meta.merge(meta))
21
- end
22
-
23
- # Combine with other relations
24
- #
25
- # @overload combine(*associations)
26
- # Composes relations using configured associations
27
- # @example
28
- # users.combine(:tasks, :posts)
29
- # @param *associations [Array<Symbol>] A list of association names
30
- #
31
- # @overload combine(options)
32
- # Composes relations based on options
33
- #
34
- # @example
35
- # # users have-many tasks (name and join-keys inferred, which needs associations in schema)
36
- # users.combine(many: tasks)
37
- #
38
- # # users have-many tasks with custom name (join-keys inferred, which needs associations in schema)
39
- # users.combine(many: { priority_tasks: tasks.priority })
40
- #
41
- # # users have-many tasks with custom view and join keys
42
- # users.combine(many: { tasks: [tasks.for_users, id: :task_id] })
43
- #
44
- # # users has-one task
45
- # users.combine(one: { task: tasks })
46
- #
47
- # @param options [Hash] Options for combine
48
- # @option :many [Hash] Sets options for "has-many" type of association
49
- # @option :one [Hash] Sets options for "has-one/belongs-to" type of association
50
- #
51
- # @return [RelationProxy]
52
- #
53
- # @api public
54
- def combine(*args)
55
- options = args[0].is_a?(Hash) ? args[0] : args
56
-
57
- combine_opts = Hash.new { |h, k| h[k] = {} }
58
-
59
- options.each do |key, value|
60
- if key == :one || key == :many
61
- if value.is_a?(Hash)
62
- value.each do |name, spec|
63
- if spec.is_a?(Array)
64
- combine_opts[key][name] = spec
65
- else
66
- _, (curried, keys) = combine_opts_from_relations(spec).to_a[0]
67
- combine_opts[key][name] = [curried, keys]
68
- end
69
- end
70
- else
71
- _, (curried, keys) = combine_opts_from_relations(value).to_a[0]
72
- combine_opts[key][curried.combine_tuple_key(key)] = [curried, keys]
73
- end
74
- else
75
- if value.is_a?(Array)
76
- other =
77
- if registry.key?(key)
78
- registry[key]
79
- else
80
- registry[associations[key].target]
81
- end
82
- curried = combine_from_assoc(key, other).combine(*value)
83
- result, _, keys = combine_opts_for_assoc(key)
84
- combine_opts[result][key] = [curried, keys]
85
- else
86
- result, curried, keys = combine_opts_for_assoc(key, value)
87
- combine_opts[result][key] = [curried, keys]
88
- end
89
- end
90
- end
91
-
92
- nodes = combine_opts.flat_map do |type, relations|
93
- relations.map { |name, (relation, keys)|
94
- relation.combined(name, keys, type)
95
- }
96
- end
97
-
98
- __new__(relation.graph(*nodes))
99
- end
100
-
101
- # Shortcut for combining with parents which infers the join keys
102
- #
103
- # @example
104
- # # tasks belong-to users
105
- # tasks.combine_parents(one: users)
106
- #
107
- # # tasks belong-to users with custom user view
108
- # tasks.combine_parents(one: users.task_owners)
109
- #
110
- # @param options [Hash] Combine options hash
111
- #
112
- # @return [RelationProxy]
113
- #
114
- # @api public
115
- def combine_parents(options)
116
- combine_opts = {}
117
-
118
- options.each do |type, parents|
119
- combine_opts[type] =
120
- case parents
121
- when Hash
122
- parents.each_with_object({}) { |(name, parent), r|
123
- keys = combine_keys(parent, relation, :parent)
124
- curried = combine_from_assoc_with_fallback(name, parent, keys)
125
- r[name] = [curried, keys]
126
- }
127
- when Array
128
- parents.each_with_object({}) { |parent, r|
129
- keys = combine_keys(parent, relation, :parent)
130
- tuple_key = parent.combine_tuple_key(type)
131
- curried = combine_from_assoc_with_fallback(parent.name, parent, keys)
132
- r[tuple_key] = [curried, keys]
133
- }
134
- else
135
- keys = combine_keys(parents, relation, :parent)
136
- tuple_key = parents.combine_tuple_key(type)
137
- curried = combine_from_assoc_with_fallback(parents.name, parents, keys)
138
- { tuple_key => [curried, keys] }
139
- end
140
- end
141
-
142
- combine(combine_opts)
143
- end
144
-
145
- # Shortcut for combining with children which infers the join keys
146
- #
147
- # @example
148
- # # users have-many tasks
149
- # users.combine_children(many: tasks)
150
- #
151
- # # users have-many tasks with custom mapping (requires associations)
152
- # users.combine_children(many: { priority_tasks: tasks.priority })
153
- #
154
- # @param [Hash] options
155
- #
156
- # @return [RelationProxy]
157
- #
158
- # @api public
159
- def combine_children(options)
160
- combine_opts = {}
161
-
162
- options.each do |type, children|
163
- combine_opts[type] =
164
- case children
165
- when Hash
166
- children.each_with_object({}) { |(name, child), r|
167
- keys = combine_keys(relation, child, :children)
168
- curried = combine_from_assoc_with_fallback(name, child, keys)
169
- r[name] = [curried, keys]
170
- }
171
- when Array
172
- children.each_with_object({}) { |child, r|
173
- keys = combine_keys(relation, child, :children)
174
- tuple_key = child.combine_tuple_key(type)
175
- curried = combine_from_assoc_with_fallback(child.name, child, keys)
176
- r[tuple_key] = [curried, keys]
177
- }
178
- else
179
- keys = combine_keys(relation, children, :children)
180
- curried = combine_from_assoc_with_fallback(children.name, children, keys)
181
- tuple_key = children.combine_tuple_key(type)
182
- { tuple_key => [curried, keys] }
183
- end
184
- end
185
-
186
- combine(combine_opts)
187
- end
188
-
189
- protected
190
-
191
- # Infer join/combine keys for a given relation and association type
192
- #
193
- # When source has association corresponding to target's name, it'll be
194
- # used to get the keys. Otherwise we fall back to using default keys based
195
- # on naming conventions.
196
- #
197
- # @param [Relation::Name] source The source relation name
198
- # @param [Relation::Name] target The target relation name
199
- # @param [Symbol] type The association type, can be either :parent or :children
200
- #
201
- # @return [Hash<Symbol=>Symbol>]
202
- #
203
- # @api private
204
- def combine_keys(source, target, type)
205
- source.associations.try(target.name) { |assoc|
206
- assoc.combine_keys(__registry__)
207
- } or infer_combine_keys(source, target, type)
208
- end
209
-
210
- # Build combine options from a relation mapping hash passed to `combine`
211
- #
212
- # This method will infer combine keys either from defined associations
213
- # or use the keys provided explicitly for ad-hoc combines
214
- #
215
- # It returns a mapping like `name => [preloadable_relation, combine_keys]`
216
- # and this mapping is used by `combine` to build a full relation graph
217
- #
218
- # @api private
219
- def combine_opts_from_relations(*relations)
220
- relations.each_with_object({}) do |spec, h|
221
- # We assume it's a child relation
222
- keys = combine_keys(relation, spec, :children)
223
- rel = combine_from_assoc_with_fallback(spec.name, spec, keys)
224
- h[spec.name.relation] = [rel, keys]
225
- end
226
- end
227
-
228
- # @api private
229
- def combine_from_assoc_with_fallback(name, other, keys)
230
- combine_from_assoc(name, other) do
231
- other.combine_method(relation, keys)
232
- end
233
- end
234
-
235
- # Try to get a preloadable relation from a defined association
236
- #
237
- # If association doesn't exist we call the fallback block
238
- #
239
- # @return [RelationProxy]
240
- #
241
- # @api private
242
- def combine_from_assoc(name, other, &fallback)
243
- return other if other.curried?
244
- associations.try(name) { |assoc| other.for_combine(assoc) } or fallback.call
245
- end
246
-
247
- # Extract result (either :one or :many), preloadable relation and its keys
248
- # by using given association name
249
- #
250
- # This is used when a flat list of association names was passed to `combine`
251
- #
252
- # @api private
253
- def combine_opts_for_assoc(name, opts = nil)
254
- assoc = relation.associations[name]
255
- curried = registry[assoc.target.relation].for_combine(assoc)
256
- curried = curried.combine(opts) unless opts.nil?
257
- keys = assoc.combine_keys(__registry__)
258
- [assoc.result, curried, keys]
259
- end
260
-
261
- # Build a preloadable relation for relation graph
262
- #
263
- # When a given relation defines `for_other_relation` then it will be used
264
- # to preload `other_relation`. ie `users` relation defines `for_tasks`
265
- # then when we preload tasks for users, this custom method will be used
266
- #
267
- # This *defaults* to the built-in `for_combine` with explicitly provided
268
- # keys
269
- #
270
- # @return [RelationProxy]
271
- #
272
- # @api private
273
- def combine_method(other, keys)
274
- custom_name = :"for_#{other.name.dataset}"
275
-
276
- if relation.respond_to?(custom_name)
277
- __send__(custom_name)
278
- else
279
- for_combine(keys)
280
- end
281
- end
282
-
283
- # Infer key under which a combine relation will be loaded
284
- #
285
- # This is used in cases like ad-hoc combines where relation was passed
286
- # in without specifying the key explicitly, ie:
287
- #
288
- # tasks.combine_parents(one: users)
289
- #
290
- # # ^^^ this will be expanded under-the-hood to:
291
- # tasks.combine(one: { user: users })
292
- #
293
- # @return [Symbol]
294
- #
295
- # @api private
296
- def combine_tuple_key(result)
297
- if result == :one
298
- Dry::Core::Inflector.singularize(base_name.relation).to_sym
299
- else
300
- base_name.relation
301
- end
302
- end
303
-
304
- # Fallback mechanism for `combine_keys` when there's no association defined
305
- #
306
- # @api private
307
- def infer_combine_keys(source, target, type)
308
- primary_key = source.primary_key
309
- foreign_key = target.foreign_key(source)
310
-
311
- if type == :parent
312
- { foreign_key => primary_key }
313
- else
314
- { primary_key => foreign_key }
315
- end
316
- end
317
- end
318
- end
319
- end
320
- end