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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -0
- data/exe/ar-precompile +47 -0
- data/lib/active_record_precompiled/assoc_site.rb +65 -0
- data/lib/active_record_precompiled/build.rb +52 -0
- data/lib/active_record_precompiled/capture.rb +94 -0
- data/lib/active_record_precompiled/friendly_site.rb +49 -0
- data/lib/active_record_precompiled/keys.rb +41 -0
- data/lib/active_record_precompiled/preload.rb +619 -0
- data/lib/active_record_precompiled/railtie.rb +46 -0
- data/lib/active_record_precompiled/rewriter.rb +917 -0
- data/lib/active_record_precompiled/schema_dump.rb +181 -0
- data/lib/active_record_precompiled/scope_analyzer.rb +246 -0
- data/lib/active_record_precompiled/scope_check.rb +59 -0
- data/lib/active_record_precompiled/scopes.rb +32 -0
- data/lib/active_record_precompiled/site.rb +481 -0
- data/lib/active_record_precompiled/version.rb +5 -0
- data/lib/active_record_precompiled.rb +64 -0
- data/lib/activerecord-precompiled.rb +4 -0
- metadata +102 -0
|
@@ -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
|