activerecord-precompiled 0.1.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.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordPrecompiled
4
+ # The columns an association joins on. Rails 8.0 and 8.1 have
5
+ # join_primary_key/join_foreign_key on reflections and
6
+ # LoaderQuery#association_key_name; Rails main dropped the first two (for
7
+ # *_key_definition and association routes) and renamed the third
8
+ # destination_key_name. These answer as the old methods did, on either.
9
+ module Keys
10
+ JOIN_KEYS = ActiveRecord::Reflection::AbstractReflection.method_defined?(:join_foreign_key)
11
+
12
+ LOADER_KEY = if ActiveRecord::Associations::Preloader::Association::LoaderQuery.method_defined?(:association_key_name)
13
+ :association_key_name
14
+ else
15
+ :destination_key_name
16
+ end
17
+
18
+ class << self
19
+ # The key on the associated table: what its rows are matched on.
20
+ def join_primary_key(reflection, klass = nil)
21
+ return klass ? reflection.join_primary_key(klass) : reflection.join_primary_key if JOIN_KEYS
22
+
23
+ reflection = reflection.source_reflection while reflection.through_reflection?
24
+ return reflection.foreign_key unless reflection.belongs_to?
25
+
26
+ reflection.polymorphic? ? reflection.association_primary_key(klass) : reflection.association_primary_key
27
+ end
28
+
29
+ # The key on the owner's table: the value the associated rows match.
30
+ def join_foreign_key(reflection)
31
+ return reflection.join_foreign_key if JOIN_KEYS
32
+
33
+ reflection = reflection.source_reflection while reflection.through_reflection?
34
+ reflection.belongs_to? ? reflection.foreign_key : reflection.active_record_primary_key
35
+ end
36
+
37
+ # The column a Preloader query looks its keys up in.
38
+ def loader_key(loader_query) = loader_query.public_send(LOADER_KEY)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,619 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordPrecompiled
4
+ # Precompiled association queries for a Site's includes/preload.
5
+ #
6
+ # ActiveRecord's Preloader runs one query per association, always through
7
+ # LoaderQuery#load_records_for_keys: `scope.where(key => keys).load`. The
8
+ # build captures that query's SQL for one key (`key = ?`) and for two
9
+ # (`key IN (?, ?)`), so any number of keys gets ActiveRecord's exact SQL.
10
+ # Everything else (grouping by owner, inverses, nested and through
11
+ # associations) stays ActiveRecord's.
12
+ #
13
+ # At runtime a plan is used only when the LoaderQuery is `eql?` to one built
14
+ # from the same reflection: the identity ActiveRecord itself uses to batch
15
+ # identical association queries.
16
+ module Preload
17
+ # Above this, building records dominates and the gain is small; it also
18
+ # keeps well clear of SQLite's bind parameter limit.
19
+ MAX_KEYS = 100
20
+ THREAD_KEY = :__ar_precompiled_preload
21
+ # Placeholder keys for the build-time capture; recognised among the binds.
22
+ PROBES = { integer: [-7_000_001, -7_000_002], string: %w[__ar_precompiled_key_0__ __ar_precompiled_key_1__] }.freeze
23
+ IN_TWO = " IN (?, ?)"
24
+
25
+ # --- build time ----------------------------------------------------------
26
+
27
+ # Plans for every association in a preload tree that has a fixed query.
28
+ # The others are left to the Preloader as is.
29
+ def self.compile(model, values)
30
+ plans = []
31
+ walk(model, values) do |owner, reflection|
32
+ if reflection.through_reflection?
33
+ plans.concat(compile_through(owner, reflection))
34
+ elsif (plan = compile_one(owner, reflection))
35
+ plans << plan
36
+ end
37
+ end
38
+ plans.uniq
39
+ end
40
+
41
+ def self.walk(model, value, &block)
42
+ case value
43
+ when Array then value.each { walk(model, it, &block) }
44
+ when Hash then value.each { |k, v| walk(model, k, &block) && walk(model._reflect_on_association(k.to_sym).klass, v, &block) }
45
+ when Symbol, String
46
+ # As the Preloader looks it up: has_and_belongs_to_many is a through association here.
47
+ reflection = model._reflect_on_association(value.to_sym) or return false
48
+ yield model, reflection
49
+ !reflection.polymorphic?
50
+ end
51
+ end
52
+
53
+ # A has_many/has_one :through runs two preloads of its own (owners to
54
+ # through records, through records to targets), with scopes it builds
55
+ # (ThroughAssociation#through_scope and #scope). Plans for those, found
56
+ # again at runtime by the same path (`via`). Nested through associations
57
+ # are left to ActiveRecord.
58
+ def self.compile_through(owner, reflection)
59
+ through = reflection.through_reflection
60
+ source = reflection.source_reflection
61
+ return [] if through.through_reflection? || source.through_reflection? || source.polymorphic?
62
+ return [] if reflection.scope && !static_scope?(reflection)
63
+
64
+ parts = through_parts(owner, reflection) or return []
65
+ via = [owner.name, reflection.name]
66
+ [
67
+ compile_one(owner, through, preload_scope: parts[:through], via: via + ["through"]),
68
+ compile_one(through.klass, source, preload_scope: parts[:source], via: via + ["source"]),
69
+ ].compact
70
+ end
71
+
72
+ # The scopes ThroughAssociation hands its two inner Preloaders.
73
+ def self.through_parts(owner, reflection)
74
+ association = ActiveRecord::Associations::Preloader::ThroughAssociation.new(
75
+ reflection.klass, [owner.allocate], reflection, nil, nil, false,
76
+ )
77
+ { through: association.send(:through_scope), source: association.send(:scope) }
78
+ rescue StandardError
79
+ nil
80
+ end
81
+
82
+ def self.compile_one(owner, reflection, preload_scope: nil, via: nil)
83
+ return if reflection.through_reflection? || reflection.polymorphic?
84
+ return if reflection.scope && !static_scope?(reflection)
85
+
86
+ klass = reflection.klass
87
+ return if klass.default_scopes.any? || !klass.table_exists?
88
+
89
+ loader = loader_for(owner, reflection, preload_scope)
90
+ # references only matters for eager_load, which a preload query isn't.
91
+ return unless loader && loader.scope.values.keys.all? { %i[where order references].include?(it) }
92
+
93
+ key = Keys.loader_key(loader)
94
+ return if key.is_a?(Array)
95
+
96
+ probes = PROBES[klass.type_for_attribute(key).type] or return
97
+ one, two = [probes.first(1), probes].map do |keys|
98
+ queries = klass.with_connection do |conn|
99
+ return unless conn.is_a?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
100
+
101
+ prepared(conn) { Capture.queries { loader.load_records_for_keys(keys) } }
102
+ end
103
+ return unless queries.size == 1
104
+
105
+ queries.first
106
+ end
107
+ # The key's own list: the scope may have IN lists of its own.
108
+ column = klass.with_connection { "#{it.quote_table_name(klass.table_name)}.#{it.quote_column_name(key)}" }
109
+ marker = "#{column}#{IN_TWO}"
110
+ return unless two.sql.scan(marker).size == 1
111
+
112
+ prefix, suffix = two.sql.split(marker, 2)
113
+ prefix += column
114
+ many = bind_specs(klass, two, probes)
115
+ at = many.index(:key)
116
+ return unless many.count(:key) == 2 && many[at + 1] == :key && bind_specs(klass, one, probes).count(:key) == 1
117
+
118
+ { owner: owner.name, name: reflection.name, schema: Site.schema_of(klass),
119
+ one: [one.sql, bind_specs(klass, one, probes)], many: [prefix, suffix, many[0, at], many[(at + 2)..]],
120
+ via: via }.compact
121
+ end
122
+
123
+ # Association scopes such as `-> { order(position: :asc) }`; scopes that
124
+ # read the time, the owner, or anything else when they run are not.
125
+ def self.static_scope?(reflection)
126
+ scope = original_scope(reflection.scope)
127
+ return false unless scope.arity.zero?
128
+
129
+ ScopeAnalyzer.new.analyze_proc(reflection.klass, scope)
130
+ true
131
+ rescue ScopeAnalyzer::NotStatic
132
+ false
133
+ end
134
+
135
+ # Rails wraps a scope without arguments as `proc { instance_exec(&scope) }`
136
+ # (Associations::Builder::Association.build_scope); analyze the original.
137
+ def self.original_scope(scope)
138
+ wrapped = scope.source_location&.first.to_s.end_with?("active_record/associations/builder/association.rb")
139
+ wrapped && scope.binding.local_variable_defined?(:scope) ? scope.binding.local_variable_get(:scope) : scope
140
+ end
141
+
142
+ def self.bind_specs(klass, query, probes)
143
+ query.binds.map do |bind|
144
+ next :key if probes.include?(bind.value_before_type_cast)
145
+
146
+ value = klass.with_connection { Site.db_constant(it, bind.value_for_database) }
147
+ return [] if value.equal?(Site::FALLBACK) # no plan: count(:key) won't match
148
+
149
+ [:val, value, bind.name]
150
+ end
151
+ end
152
+
153
+ def self.prepared(conn)
154
+ previous = conn.instance_variable_get(:@prepared_statements)
155
+ conn.instance_variable_set(:@prepared_statements, true)
156
+ yield
157
+ ensure
158
+ conn.instance_variable_set(:@prepared_statements, previous)
159
+ end
160
+
161
+ # The LoaderQuery the Preloader would build for this reflection. The
162
+ # owner is only asked for its class, so it needn't be initialized.
163
+ def self.loader_for(owner, reflection, preload_scope = nil)
164
+ association = ActiveRecord::Associations::Preloader::Association.new(
165
+ reflection.klass, [owner.allocate], reflection, preload_scope, nil, false,
166
+ )
167
+ association.send(:loader_query)
168
+ end
169
+
170
+ # --- runtime ---------------------------------------------------------------
171
+
172
+ class Plan
173
+ PLANS = {}
174
+
175
+ # Sites that preload the same association share one plan.
176
+ def self.for(definition)
177
+ definition = definition.transform_values { ActiveRecordPrecompiled.intern(it) }.freeze
178
+ PLANS[definition] ||= new(**definition)
179
+ end
180
+
181
+ # `via` is [owner, through association, "through" or "source"] for the
182
+ # inner preloads of a through association.
183
+ def initialize(owner:, name:, schema:, one:, many:, via: nil)
184
+ @owner_name = owner
185
+ @name = name
186
+ @via = via
187
+ @schema = schema
188
+ @one_sql, @one_binds = one
189
+ @prefix, @suffix, @before, @after = many
190
+ end
191
+
192
+ attr_reader :owner_name, :name, :via
193
+
194
+ # The verified reference, computed once; false if this process would
195
+ # build different SQL.
196
+ def loader_query
197
+ return @loader_query if defined?(@loader_query)
198
+
199
+ @loader_query = reference || false
200
+ end
201
+
202
+ # The LoaderQuery this plan answers for, or nil if this process would
203
+ # build different SQL than the build captured (model or schema changed):
204
+ # then the plan is never used. Checked once per Site.
205
+ def reference
206
+ owner, scope = @via ? via_owner_and_scope : [constant(@owner_name), nil]
207
+ return unless owner
208
+
209
+ reflection = owner._reflect_on_association(@name)
210
+ return unless reflection && Site.schema_of(reflection.klass) == @schema
211
+
212
+ loader = Preload.loader_for(owner, reflection, scope)
213
+ loader if Preload.sql_for_one_key(loader) == @one_sql
214
+ end
215
+
216
+ # The model this inner preload of a through association runs on (the
217
+ # join model of a has_and_belongs_to_many is anonymous), and the preload
218
+ # scope ThroughAssociation passes it.
219
+ private def via_owner_and_scope
220
+ through_owner = constant(@via[0]) or return nil
221
+ through = through_owner._reflect_on_association(@via[1])
222
+ return nil unless through&.through_reflection?
223
+
224
+ parts = Preload.through_parts(through_owner, through) or return nil
225
+ if @via[2] == "through"
226
+ [through_owner, parts[:through]]
227
+ else
228
+ [through.through_reflection.klass, parts[:source]]
229
+ end
230
+ end
231
+
232
+ private def constant(name)
233
+ Object.const_get(name)
234
+ rescue NameError
235
+ nil
236
+ end
237
+
238
+ # Records for `keys`, as `scope.where(key => keys).load(&block)` would
239
+ # return them; Site::FALLBACK when a key isn't a plain value.
240
+ def load(loader_query, keys, &block)
241
+ load_keys(loader_query.scope.klass, Keys.loader_key(loader_query), keys, &block)
242
+ end
243
+
244
+ def load_keys(klass, key_name, keys, &block)
245
+ type = klass.type_for_attribute(key_name)
246
+ klass.with_connection do |conn|
247
+ return Site::FALLBACK unless conn.is_a?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
248
+
249
+ values = keys.map do |key|
250
+ value = type.serialize(key)
251
+ return Site::FALLBACK if value.nil? || !Site::SCALARS.any? { it === value }
252
+
253
+ Site.db_value(conn, value)
254
+ end
255
+ sql, binds = statement_for(values)
256
+ columns, rows = Site.execute(conn, sql, binds, "#{klass.name} Load", self, [klass, key_name, values.size])
257
+ records = klass._load_from_sql(ActiveRecord::Result.new(columns, rows), &block)
258
+ Site.lazy_preload(records)
259
+ records
260
+ end
261
+ end
262
+
263
+ private
264
+
265
+ def statement_for(values)
266
+ if values.size == 1
267
+ [@one_sql, @one_binds.map { it == :key ? values[0] : it[1] }]
268
+ else
269
+ sql = (@sql_by_count ||= {})[values.size] ||= "#{@prefix} IN (#{Array.new(values.size, "?").join(", ")})#{@suffix}"
270
+ [sql, @before.map { it[1] } + values + @after.map { it[1] }]
271
+ end
272
+ end
273
+
274
+ public
275
+
276
+ # [name, type] per bind, for the `sql.active_record` payload.
277
+ def bind_names((klass, key_name, count))
278
+ key = [key_name, klass.type_for_attribute(key_name)]
279
+ constant = ->(spec) { [spec[2], ActiveModel::Type.default_value] }
280
+ if count == 1
281
+ @one_binds.map { it == :key ? key : constant.(it) }
282
+ else
283
+ @before.map(&constant) + Array.new(count, key) + @after.map(&constant)
284
+ end
285
+ end
286
+ end
287
+
288
+ # LoaderQuery => Plan for a Site's verified plans, built on first use.
289
+ def self.registry(plans)
290
+ # Equal LoaderQueries (the same table, key and scope) share one plan:
291
+ # they run the same SQL.
292
+ plans.each_with_object({}) do |plan, registry|
293
+ (query = plan.loader_query) && (registry[query] ||= plan)
294
+ end
295
+ end
296
+
297
+ # ActiveRecord's SQL for one key, as the preloader would send it with
298
+ # prepared statements on.
299
+ def self.sql_for_one_key(loader)
300
+ klass = loader.scope.klass
301
+ key = Keys.loader_key(loader)
302
+ probe = PROBES[klass.type_for_attribute(key).type]&.first or return
303
+ klass.with_connection do |conn|
304
+ prepared(conn) { conn.__send__(:to_sql_and_binds, loader.scope.where(key => [probe]).arel, [], nil).first }
305
+ end
306
+ end
307
+
308
+ # A preload tree run without the Preloader's objects, for trees where
309
+ # every association has a verified plan. It is Preloader::Batch in
310
+ # miniature, and does what Batch, Branch, Association and
311
+ # ThroughAssociation do, in the same order: rounds of runnable loaders,
312
+ # loaders deferred while another branch will still load their table,
313
+ # loaders with equal LoaderQueries loaded by one query (so records are
314
+ # shared between associations as ActiveRecord shares them), owners
315
+ # grouped by key, inverses, already-loaded targets.
316
+ class Tree
317
+ # A through association has no plan of its own: `through` and `source`
318
+ # are the two inner preloads ThroughAssociation runs.
319
+ Node = Struct.new(:reflection, :plan, :owner_key, :key, :convert, :children, :through, :source, :future)
320
+
321
+ # nil when the tree doesn't qualify; the Site then uses the Preloader.
322
+ # `plans`: [owner, association] => Plan; `inner`: [owner, association,
323
+ # "through" | "source"] => Plan for through associations.
324
+ def self.build(model, value, plans, inner = {})
325
+ nodes = nodes_for(model, value, plans, inner)
326
+ nodes ? new(nodes) : nil
327
+ end
328
+
329
+ def self.nodes_for(model, value, plans, inner)
330
+ return nil unless model.descendants.empty? # STI subclasses may redefine associations
331
+
332
+ case value
333
+ when Array
334
+ nodes = value.map { nodes_for(model, it, plans, inner) }
335
+ nodes.all? ? nodes.flatten : nil
336
+ when Hash
337
+ value.map do |name, children|
338
+ node = node_for(model, name, plans, inner) or return nil
339
+ node.children = nodes_for(node.reflection.klass, children, plans, inner) or return nil
340
+ node
341
+ end
342
+ when Symbol, String
343
+ (node = node_for(model, value, plans, inner)) ? [node] : nil
344
+ end
345
+ end
346
+
347
+ def self.node_for(model, name, plans, inner)
348
+ reflection = model._reflect_on_association(name) or return nil
349
+ return nil if reflection.respond_to?(:deprecated?) && reflection.deprecated?
350
+ return through_node(model, reflection, inner) if reflection.through_reflection?
351
+
352
+ plan = plans[[model.name, reflection.name]] or return nil
353
+ plain_node(model, reflection, plan)
354
+ end
355
+
356
+ def self.plain_node(model, reflection, plan)
357
+ klass = reflection.klass
358
+ key = Keys.join_primary_key(reflection, klass).to_s
359
+ owner_key = Keys.join_foreign_key(reflection).to_s
360
+ convert = klass.type_for_attribute(key).type != model.type_for_attribute(owner_key).type
361
+ Node.new(reflection, plan, owner_key, key, convert, [])
362
+ end
363
+
364
+ # Only when both inner preloads assign what they load (their scopes are
365
+ # empty: no conditions, order or distinct on the through association),
366
+ # so an owner's targets are the source records of its through records.
367
+ def self.through_node(model, reflection, inner)
368
+ through = reflection.through_reflection
369
+ source = reflection.source_reflection
370
+ return nil if through.through_reflection? || source.through_reflection? || source.polymorphic?
371
+ return nil unless through.klass.descendants.empty?
372
+
373
+ parts = Preload.through_parts(model, reflection) or return nil
374
+ return nil unless parts[:through].empty_scope? && parts[:source].empty_scope?
375
+
376
+ through_plan = inner[[model.name, reflection.name, "through"]] or return nil
377
+ source_plan = inner[[model.name, reflection.name, "source"]] or return nil
378
+ Node.new(reflection, nil, nil, nil, nil, [], plain_node(model, through, through_plan), plain_node(through.klass, source, source_plan))
379
+ end
380
+
381
+ def initialize(nodes)
382
+ @nodes = nodes
383
+ nodes.each { compute_future(it) }
384
+ end
385
+
386
+ # What a branch that hasn't started yet will load: Branch#future_classes
387
+ # through #likely_reflections (each reflection's chain), and its children's.
388
+ def compute_future(node)
389
+ node.children.each { compute_future(it) }
390
+ node.future = (node.reflection.chain.map(&:klass) + node.children.flat_map(&:future)).uniq
391
+ end
392
+
393
+ # Preloader::Batch#call.
394
+ def call(records, registry)
395
+ branches = @nodes.map { Branch.new(it, records) }
396
+ until branches.empty?
397
+ loaders = branches.flat_map(&:runnable_loaders)
398
+ if loaders.any?
399
+ future_tables = branches.flat_map { |branch| branch.future_classes - branch.runnable_loaders.map(&:klass) }.map(&:table_name).uniq
400
+ targets = loaders.reject { future_tables.include?(it.klass.table_name) }
401
+ targets = loaders if targets.empty?
402
+
403
+ targets.grep(Loader).group_by { [it.loader_query, it.klass] }.each_value { load_batch(it) }
404
+ targets.each(&:run)
405
+ end
406
+ finished, in_progress = branches.partition(&:done?)
407
+ branches = in_progress + finished.flat_map(&:children)
408
+ end
409
+ end
410
+
411
+ private
412
+
413
+ # LoaderQuery#load_records_in_batch with LoaderRecords: one query for
414
+ # the keys of every loader in the group, plus their loaded targets.
415
+ def load_batch(loaders)
416
+ keys = {} # an ordered set
417
+ already = {}
418
+ loaders.each do |loader|
419
+ loader.owners_by_key.each do |key, owners|
420
+ if (loaded_owner = owners.find { loader.loaded?(it) })
421
+ already[key] = loader.target_for(loaded_owner)
422
+ else
423
+ keys[key] = true
424
+ end
425
+ end
426
+ end
427
+ keys = keys.keys - already.keys
428
+
429
+ set_inverse = ->(record) { loaders.each { it.set_inverse(record) } }
430
+ first = loaders.first
431
+ fetched = keys.empty? ? [] : first.node.plan.load_keys(first.klass, first.node.key, keys, &set_inverse) if keys.size <= MAX_KEYS
432
+ # Too many keys, or one that isn't a plain value: ActiveRecord's own query.
433
+ fetched = first.loader_query.load_records_for_keys(keys, &set_inverse).to_a if fetched.nil? || fetched.equal?(Site::FALLBACK)
434
+
435
+ raw = fetched + already.values.flatten
436
+ loaders.each do |loader|
437
+ loader.load_records(raw)
438
+ loader.run
439
+ end
440
+ end
441
+
442
+ # Preloader::Branch for one association of the tree.
443
+ class Branch
444
+ def initialize(node, owners)
445
+ @node = node
446
+ @loader = node.through ? ThroughLoader.new(node, owners) : Loader.new(node, owners)
447
+ end
448
+
449
+ def runnable_loaders = @loader.runnable_loaders.reject(&:run?)
450
+ def done? = @loader.run?
451
+ def future_classes = (@loader.future_classes + @node.children.flat_map(&:future)).uniq
452
+ def children = @node.children.map { Branch.new(it, @loader.preloaded_records) }
453
+ end
454
+
455
+ # Preloader::Association.
456
+ class Loader
457
+ attr_reader :node, :klass, :owners
458
+
459
+ def initialize(node, owners)
460
+ @node = node
461
+ @klass = node.reflection.klass
462
+ @owners = owners.uniq(&:__id__)
463
+ @name = node.reflection.name
464
+ @run = false
465
+ end
466
+
467
+ def loader_query = @node.plan.loader_query
468
+ def run? = @run
469
+ def runnable_loaders = [self]
470
+ def future_classes = run? ? [] : [klass]
471
+ def loaded?(owner) = owner.association(@name).loaded?
472
+ def target_for(owner) = Array.wrap(owner.association(@name).target)
473
+
474
+ def owners_by_key
475
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
476
+ key = key_of(owner, @node.owner_key)
477
+ (result[key] ||= []) << owner if key
478
+ end
479
+ end
480
+
481
+ def set_inverse(record)
482
+ if (owners = owners_by_key[key_of(record, @node.key)])
483
+ owners.first.association(@name).set_inverse_instance(record)
484
+ end
485
+ end
486
+
487
+ def load_records(raw)
488
+ @records_by_owner = {}.compare_by_identity
489
+ collection = @node.reflection.collection?
490
+ @preloaded_records = raw.select do |record|
491
+ assigned = false
492
+ owners_by_key[key_of(record, @node.key)]&.each do |owner|
493
+ entries = (@records_by_owner[owner] ||= [])
494
+ if collection || entries.empty?
495
+ entries << record
496
+ assigned = true
497
+ end
498
+ end
499
+ assigned
500
+ end
501
+ end
502
+
503
+ def records_by_owner = @records_by_owner || {}
504
+ def preloaded_records = @preloaded_records || []
505
+
506
+ def run
507
+ return self if run?
508
+
509
+ @run = true
510
+ owners.each { Tree.assign(it.association(@name), @node.reflection, records_by_owner[it] || []) }
511
+ self
512
+ end
513
+
514
+ private
515
+
516
+ def key_of(record, attribute)
517
+ key = record._read_attribute(attribute)
518
+ @node.convert ? key.to_s : key
519
+ end
520
+ end
521
+
522
+ # Preloader::ThroughAssociation, whose two inner preloads assign.
523
+ class ThroughLoader
524
+ attr_reader :klass, :owners
525
+
526
+ def initialize(node, owners)
527
+ @node = node
528
+ @klass = node.reflection.klass
529
+ @owners = owners.uniq(&:__id__)
530
+ @name = node.reflection.name
531
+ @through = Loader.new(node.through, @owners)
532
+ @run = false
533
+ end
534
+
535
+ def run? = @run
536
+ def loaded?(owner) = owner.association(@name).loaded?
537
+
538
+ # Inner Preloader for the source: none when there are no through records.
539
+ def source
540
+ return @source if defined?(@source)
541
+
542
+ middle = @through.records_by_owner.values.flatten
543
+ @source = middle.empty? ? nil : Loader.new(@node.source, middle)
544
+ end
545
+
546
+ def data_available?
547
+ owners.all? { loaded?(it) } || (@through.run? && (source.nil? || source.run?))
548
+ end
549
+
550
+ def runnable_loaders
551
+ if data_available? then [self]
552
+ elsif @through.run? then source.runnable_loaders
553
+ else @through.runnable_loaders
554
+ end
555
+ end
556
+
557
+ def future_classes
558
+ if run? then []
559
+ elsif @through.run? then source ? source.future_classes : []
560
+ else (@through.future_classes + @node.reflection.source_reflection.chain.map(&:klass)).uniq
561
+ end
562
+ end
563
+
564
+ def preloaded_records = source ? source.preloaded_records : []
565
+
566
+ def run
567
+ return self if run?
568
+
569
+ @run = true
570
+ source_by_owner = source ? source.records_by_owner : {}
571
+ owners.each do |owner|
572
+ association = owner.association(@name)
573
+ next if association.loaded?
574
+
575
+ records = (@through.records_by_owner[owner] || []).flat_map { source_by_owner[it] || [] }
576
+ Tree.assign(association, @node.reflection, records)
577
+ end
578
+ self
579
+ end
580
+ end
581
+
582
+ # Association#associate_records_to_owner.
583
+ def self.assign(association, reflection, records)
584
+ return if association.loaded?
585
+
586
+ if reflection.collection?
587
+ association.target = records + association.target.reject(&:persisted?)
588
+ else
589
+ association.target = records.first
590
+ end
591
+ end
592
+ end
593
+
594
+ # Makes plans available to load_records_for_keys while the block runs.
595
+ def self.with(registry)
596
+ previous = Thread.current[THREAD_KEY]
597
+ Thread.current[THREAD_KEY] = registry
598
+ yield
599
+ ensure
600
+ Thread.current[THREAD_KEY] = previous
601
+ end
602
+
603
+ module Hook
604
+ def load_records_for_keys(keys, &block)
605
+ registry = Thread.current[Preload::THREAD_KEY]
606
+ return super unless registry && !keys.empty? && keys.size <= Preload::MAX_KEYS
607
+ return super unless (plan = registry[self])
608
+
609
+ records = plan.load(self, keys.to_a, &block)
610
+ records.equal?(Site::FALLBACK) ? super : records
611
+ end
612
+ end
613
+
614
+ def self.install!
615
+ loader = ActiveRecord::Associations::Preloader::Association::LoaderQuery
616
+ loader.prepend(Hook) unless loader <= Hook
617
+ end
618
+ end
619
+ end