torque-postgresql 4.0.0 → 4.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 +4 -4
- data/Rakefile +24 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/composite.rb +116 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
- data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
- data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
- data/lib/torque/postgresql/adapter/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +28 -2
- data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
- data/lib/torque/postgresql/adapter.rb +2 -5
- data/lib/torque/postgresql/arel/nodes.rb +63 -1
- data/lib/torque/postgresql/arel/visitors.rb +19 -8
- data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
- data/lib/torque/postgresql/associations.rb +3 -0
- data/lib/torque/postgresql/attributes/base.rb +94 -0
- data/lib/torque/postgresql/attributes/composite.rb +102 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -2
- data/lib/torque/postgresql/attributes/lquery.rb +180 -0
- data/lib/torque/postgresql/attributes/ltree.rb +169 -0
- data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
- data/lib/torque/postgresql/attributes/struct.rb +137 -0
- data/lib/torque/postgresql/base.rb +17 -14
- data/lib/torque/postgresql/config.rb +81 -14
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +138 -65
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
- data/lib/torque/postgresql/predicate_builder.rb +26 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +35 -0
- data/lib/torque/postgresql/relation/inheritance.rb +146 -28
- data/lib/torque/postgresql/relation/join_series.rb +8 -6
- data/lib/torque/postgresql/relation/merger.rb +23 -7
- data/lib/torque/postgresql/relation.rb +12 -11
- data/lib/torque/postgresql/schema_cache.rb +1 -0
- data/lib/torque/postgresql/validations.rb +29 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +1 -1
- data/lib/torque/postgresql.rb +6 -0
- data/spec/initialize.rb +20 -2
- data/spec/mocks/cache_query.rb +12 -0
- data/spec/models/author.rb +1 -1
- data/spec/models/comment.rb +2 -0
- data/spec/models/place.rb +2 -0
- data/spec/models/profile.rb +31 -0
- data/spec/schema.rb +34 -4
- data/spec/tests/arel_spec.rb +5 -3
- data/spec/tests/composite_spec.rb +800 -0
- data/spec/tests/ltree_spec.rb +552 -0
- data/spec/tests/struct_spec.rb +968 -0
- data/spec/tests/table_inheritance_spec.rb +1027 -56
- metadata +49 -7
|
@@ -104,6 +104,265 @@ RSpec.describe 'TableInheritance' do
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
context 'on syncing features' do
|
|
108
|
+
let(:parent) { 'sync_parents' }
|
|
109
|
+
let(:child) { 'sync_children' }
|
|
110
|
+
let(:migration) { ActiveRecord::Migration::Current.new('Testing') }
|
|
111
|
+
|
|
112
|
+
before do
|
|
113
|
+
allow_any_instance_of(ActiveRecord::Migration).to receive(:puts)
|
|
114
|
+
|
|
115
|
+
connection.create_table(:sync_targets) { |t| t.string :name }
|
|
116
|
+
connection.create_table(parent) do |t|
|
|
117
|
+
t.string :title
|
|
118
|
+
t.string :code
|
|
119
|
+
t.integer :author_id
|
|
120
|
+
t.tsrange :during
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
connection.add_index parent, :title
|
|
124
|
+
connection.add_unique_constraint parent, [:code]
|
|
125
|
+
connection.add_exclusion_constraint parent, 'during WITH &&', using: :gist
|
|
126
|
+
connection.add_foreign_key parent, :sync_targets, column: :author_id
|
|
127
|
+
|
|
128
|
+
connection.create_table(child, inherits: parent) { |t| t.string :extra }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'copies the primary key, which postgresql does not inherit' do
|
|
132
|
+
expect(connection.primary_keys(child)).to be_empty
|
|
133
|
+
connection.sync_inheritance_features(parent)
|
|
134
|
+
expect(connection.primary_keys(child)).to eql(%w[id])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'copies the indexes of the parent' do
|
|
138
|
+
connection.sync_inheritance_features(parent)
|
|
139
|
+
expect(connection.indexes(child).map(&:columns)).to include(%w[title])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'copies the unique constraints of the parent' do
|
|
143
|
+
connection.sync_inheritance_features(parent)
|
|
144
|
+
expect(connection.unique_constraints(child).map(&:column)).to eql([%w[code]])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'copies the exclusion constraints of the parent' do
|
|
148
|
+
connection.sync_inheritance_features(parent)
|
|
149
|
+
expect(connection.exclusion_constraints(child).map(&:expression)).to eql(['during WITH &&'])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'copies the outgoing foreign keys of the parent' do
|
|
153
|
+
connection.sync_inheritance_features(parent)
|
|
154
|
+
expect(connection.foreign_keys(child).map(&:to_table)).to eql(%w[sync_targets])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'excludes a feature that was asked for as false' do
|
|
158
|
+
connection.sync_inheritance_features(parent, primary_key: false)
|
|
159
|
+
|
|
160
|
+
expect(connection.primary_keys(child)).to be_empty
|
|
161
|
+
expect(connection.indexes(child).map(&:columns)).to include(%w[title])
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'takes any feature asked for as true as the whole selection' do
|
|
165
|
+
connection.sync_inheritance_features(parent, indexes: true)
|
|
166
|
+
|
|
167
|
+
expect(connection.indexes(child).map(&:columns)).to include(%w[title])
|
|
168
|
+
expect(connection.primary_keys(child)).to be_empty
|
|
169
|
+
expect(connection.unique_constraints(child)).to be_empty
|
|
170
|
+
expect(connection.foreign_keys(child)).to be_empty
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'refuses a feature it does not know about' do
|
|
174
|
+
expect do
|
|
175
|
+
connection.sync_inheritance_features(parent, comments: true)
|
|
176
|
+
end.to raise_error(ArgumentError, /comments/)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'names every copy after the marker' do
|
|
180
|
+
connection.sync_inheritance_features(parent)
|
|
181
|
+
names = connection.indexes(child).map(&:name) + connection.foreign_keys(child).map(&:name)
|
|
182
|
+
|
|
183
|
+
expect(names).to all(match(/\Async_inh_[0-9a-f]{10}\z/))
|
|
184
|
+
expect(names.map(&:size)).to all(be(19))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'gives a copy the same name every single time' do
|
|
188
|
+
connection.sync_inheritance_features(parent, indexes: true)
|
|
189
|
+
names = connection.indexes(child).map(&:name).sort
|
|
190
|
+
|
|
191
|
+
connection.indexes(child).each { |item| connection.remove_index(child, name: item.name) }
|
|
192
|
+
connection.sync_inheritance_features(parent, indexes: true)
|
|
193
|
+
|
|
194
|
+
expect(connection.indexes(child).map(&:name).sort).to eql(names)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'does not issue any statement on a second run' do
|
|
198
|
+
connection.sync_inheritance_features(parent)
|
|
199
|
+
|
|
200
|
+
statements = []
|
|
201
|
+
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, payload|
|
|
202
|
+
statements << payload[:sql] if payload[:sql].match?(/\A(CREATE|ALTER|DROP)/i)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
connection.sync_inheritance_features(parent)
|
|
206
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
207
|
+
|
|
208
|
+
expect(statements).to be_empty
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'leaves an equivalent index the child already has alone' do
|
|
212
|
+
connection.add_index child, :title, name: 'written_by_hand'
|
|
213
|
+
connection.sync_inheritance_features(parent)
|
|
214
|
+
|
|
215
|
+
titles = connection.indexes(child).select { |item| item.columns == %w[title] }
|
|
216
|
+
expect(titles.map(&:name)).to eql(%w[written_by_hand])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'copies the index that backs a constraint only through the constraint' do
|
|
220
|
+
connection.sync_inheritance_features(parent)
|
|
221
|
+
|
|
222
|
+
codes = connection.indexes(child).select { |item| item.columns == %w[code] }
|
|
223
|
+
expect(codes.size).to be(1)
|
|
224
|
+
expect(codes.first.name).to eql(connection.unique_constraints(child).first.name)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'cascades, carrying what each level added of its own' do
|
|
228
|
+
connection.add_index child, :extra
|
|
229
|
+
connection.create_table(:sync_grandchildren, inherits: child)
|
|
230
|
+
|
|
231
|
+
connection.sync_inheritance_features(parent)
|
|
232
|
+
|
|
233
|
+
columns = connection.indexes('sync_grandchildren').map(&:columns)
|
|
234
|
+
expect(columns).to include(%w[title])
|
|
235
|
+
expect(columns).to include(%w[extra])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'writes only to the children it was given' do
|
|
239
|
+
connection.create_table(:sync_grandchildren, inherits: child)
|
|
240
|
+
connection.sync_inheritance_features(parent, [child])
|
|
241
|
+
|
|
242
|
+
expect(connection.indexes(child).map(&:columns)).to include(%w[title])
|
|
243
|
+
expect(connection.indexes('sync_grandchildren')).to be_empty
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'pulls only from the parents that are inside the requested tree' do
|
|
247
|
+
connection.create_table(:sync_others, id: false) { |t| t.string :label }
|
|
248
|
+
connection.add_index :sync_others, :label
|
|
249
|
+
connection.create_table(:sync_mixed, inherits: [parent, :sync_others])
|
|
250
|
+
|
|
251
|
+
connection.sync_inheritance_features(parent)
|
|
252
|
+
|
|
253
|
+
columns = connection.indexes('sync_mixed').map(&:columns)
|
|
254
|
+
expect(columns).to include(%w[title])
|
|
255
|
+
expect(columns).not_to include(%w[label])
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'refuses a table that does not inherit from the parent' do
|
|
259
|
+
expect do
|
|
260
|
+
connection.sync_inheritance_features(parent, %w[authors])
|
|
261
|
+
end.to raise_error(ArgumentError, /authors/)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'drops a copy whose source is gone when pruning' do
|
|
265
|
+
connection.sync_inheritance_features(parent)
|
|
266
|
+
connection.remove_index(parent, :title)
|
|
267
|
+
connection.sync_inheritance_features(parent, prune: true)
|
|
268
|
+
|
|
269
|
+
expect(connection.indexes(child).map(&:columns)).not_to include(%w[title])
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'keeps an index written by hand when pruning, even on an inherited column' do
|
|
273
|
+
connection.add_index child, :title, name: 'written_by_hand'
|
|
274
|
+
connection.remove_index(parent, :title)
|
|
275
|
+
connection.sync_inheritance_features(parent, prune: true)
|
|
276
|
+
|
|
277
|
+
expect(connection.indexes(child).map(&:name)).to include('written_by_hand')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'never drops the primary key when pruning' do
|
|
281
|
+
connection.sync_inheritance_features(parent)
|
|
282
|
+
connection.execute("ALTER TABLE #{parent} DROP CONSTRAINT #{parent}_pkey")
|
|
283
|
+
connection.sync_inheritance_features(parent, prune: true)
|
|
284
|
+
|
|
285
|
+
expect(connection.primary_keys(child)).to eql(%w[id])
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'syncs everything when the table is created with sync' do
|
|
289
|
+
connection.create_table(:sync_others, inherits: parent, sync: true) { |t| t.string :note }
|
|
290
|
+
|
|
291
|
+
expect(connection.primary_keys('sync_others')).to eql(%w[id])
|
|
292
|
+
expect(connection.indexes('sync_others').map(&:columns)).to include(%w[title])
|
|
293
|
+
expect(connection.foreign_keys('sync_others').map(&:to_table)).to eql(%w[sync_targets])
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'syncs from every parent named by a multiple inheritance' do
|
|
297
|
+
connection.create_table(:sync_others, id: false) { |t| t.string :label }
|
|
298
|
+
connection.add_index :sync_others, :label
|
|
299
|
+
connection.create_table(:sync_mixed, inherits: [parent, :sync_others], sync: true)
|
|
300
|
+
|
|
301
|
+
columns = connection.indexes('sync_mixed').map(&:columns)
|
|
302
|
+
expect(columns).to include(%w[title])
|
|
303
|
+
expect(columns).to include(%w[label])
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'syncs only what the sync option picks' do
|
|
307
|
+
connection.create_table(:sync_others, inherits: parent, sync: { indexes: true })
|
|
308
|
+
|
|
309
|
+
expect(connection.indexes('sync_others').map(&:columns)).to include(%w[title])
|
|
310
|
+
expect(connection.primary_keys('sync_others')).to be_empty
|
|
311
|
+
expect(connection.foreign_keys('sync_others')).to be_empty
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'reverts into a sync that prunes' do
|
|
315
|
+
connection.sync_inheritance_features(parent)
|
|
316
|
+
connection.remove_index(parent, :title)
|
|
317
|
+
|
|
318
|
+
migration.revert { migration.connection.sync_inheritance_features(parent) }
|
|
319
|
+
|
|
320
|
+
expect(connection.indexes(child).map(&:columns)).not_to include(%w[title])
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'prunes only once the parent is back to what it was' do
|
|
324
|
+
connection.sync_inheritance_features(parent)
|
|
325
|
+
expect(connection.indexes(child).map(&:columns)).to include(%w[title])
|
|
326
|
+
|
|
327
|
+
migration.revert do
|
|
328
|
+
migration.connection.add_index parent, :title
|
|
329
|
+
migration.connection.sync_inheritance_features(parent)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
expect(connection.indexes(parent).map(&:columns)).not_to include(%w[title])
|
|
333
|
+
expect(connection.indexes(child).map(&:columns)).not_to include(%w[title])
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
context 'on schema' do
|
|
337
|
+
let(:dump_result) do
|
|
338
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, (io = StringIO.new))
|
|
339
|
+
io.string
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
before { connection.sync_inheritance_features(parent) }
|
|
343
|
+
|
|
344
|
+
it 'describes an inherited primary key through the sync option' do
|
|
345
|
+
line = dump_result.lines.find { |item| item.include?("create_table \"#{child}\"") }
|
|
346
|
+
|
|
347
|
+
expect(line).to include('id: false')
|
|
348
|
+
expect(line).to include(%[inherits: "#{parent}"])
|
|
349
|
+
expect(line).to match(/sync: \{:?primary_key(?:: | => |=>)true\}/)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'never describes the inherited column all over again' do
|
|
353
|
+
line = dump_result.lines.find { |item| item.include?("create_table \"#{child}\"") }
|
|
354
|
+
|
|
355
|
+
expect(line).to include('id: false')
|
|
356
|
+
expect(line).not_to match(/id: :\w+/)
|
|
357
|
+
expect(line).not_to match(/, primary_key: /)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'carries the marker of every copy, so pruning still works after a load' do
|
|
361
|
+
expect(dump_result).to match(/t\.index \["title"\], name: "sync_inh_[0-9a-f]{10}"/)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
107
366
|
context 'on schema cache' do
|
|
108
367
|
let(:schema_cache) { ActiveRecord::Base.connection.schema_cache }
|
|
109
368
|
let(:schema_cache_reflection) { schema_cache.instance_variable_get(:@schema_reflection) }
|
|
@@ -242,6 +501,32 @@ RSpec.describe 'TableInheritance' do
|
|
|
242
501
|
expect(other.casted_dependents.values.map(&:name)).to eql(%w())
|
|
243
502
|
end
|
|
244
503
|
|
|
504
|
+
it 'only considers dependents that add columns as expandable' do
|
|
505
|
+
expect(base.inheritance_expandable_dependents.keys).to \
|
|
506
|
+
eql(%w(activity_books activity_posts activity_post_samples))
|
|
507
|
+
expect(child.inheritance_expandable_dependents).to be_empty
|
|
508
|
+
expect(child2.inheritance_expandable_dependents).to be_empty
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
it 'recomputes inheritance metadata after resetting column information' do
|
|
512
|
+
base.casted_dependents
|
|
513
|
+
base.inheritance_expandable_dependents
|
|
514
|
+
base.physically_inheritances?
|
|
515
|
+
base.inheritance_column_prefix
|
|
516
|
+
base.inheritance_dependent_prefixes
|
|
517
|
+
child2.inheritance_foreign_attribute_names
|
|
518
|
+
base.reset_column_information
|
|
519
|
+
|
|
520
|
+
expect(base.instance_variable_get(:@casted_dependents)).to be_nil
|
|
521
|
+
expect(base.instance_variable_get(:@inheritance_expandable_dependents)).to be_nil
|
|
522
|
+
expect(base.instance_variable_get(:@physically_inheritances)).to be_nil
|
|
523
|
+
expect(base.instance_variable_get(:@inheritance_column_prefix)).to be_nil
|
|
524
|
+
expect(base.instance_variable_get(:@inheritance_dependent_prefixes)).to be_nil
|
|
525
|
+
expect(child2.instance_variable_get(:@inheritance_foreign_attribute_names)).to be_nil
|
|
526
|
+
expect(base.casted_dependents.values.map(&:name)).to \
|
|
527
|
+
eql(%w(ActivityBook ActivityPost ActivityPost::Sample))
|
|
528
|
+
end
|
|
529
|
+
|
|
245
530
|
it 'correctly generates the tables name' do
|
|
246
531
|
expect(base.table_name).to eql('activities')
|
|
247
532
|
expect(child.table_name).to eql('activity_posts')
|
|
@@ -263,6 +548,347 @@ RSpec.describe 'TableInheritance' do
|
|
|
263
548
|
end
|
|
264
549
|
end
|
|
265
550
|
|
|
551
|
+
context 'on partial records' do
|
|
552
|
+
let(:record) { Activity.create!(title: 'Activity test') }
|
|
553
|
+
|
|
554
|
+
it 'is neither partial nor read-only by default' do
|
|
555
|
+
expect(record.partial_record?).to be_falsey
|
|
556
|
+
expect(record.readonly?).to be_falsey
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it 'becomes read-only when marked as partial' do
|
|
560
|
+
record.send(:mark_as_partial_record!)
|
|
561
|
+
|
|
562
|
+
expect(record.partial_record?).to be_truthy
|
|
563
|
+
expect(record.readonly?).to be_truthy
|
|
564
|
+
expect { record.update!(title: 'Changed') }.to \
|
|
565
|
+
raise_error(ActiveRecord::ReadOnlyRecord)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
it 'clears both flags when marked as full' do
|
|
569
|
+
record.send(:mark_as_partial_record!)
|
|
570
|
+
record.send(:mark_as_full_record!)
|
|
571
|
+
|
|
572
|
+
expect(record.partial_record?).to be_falsey
|
|
573
|
+
expect(record.readonly?).to be_falsey
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
it 'clears the partial state on reload' do
|
|
577
|
+
record.send(:mark_as_partial_record!)
|
|
578
|
+
record.reload
|
|
579
|
+
|
|
580
|
+
expect(record.partial_record?).to be_falsey
|
|
581
|
+
expect(record.readonly?).to be_falsey
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
it 'keeps an explicit readonly through a reload on a non-inheriting model' do
|
|
585
|
+
User.create!(name: 'A user')
|
|
586
|
+
user = User.readonly.first
|
|
587
|
+
|
|
588
|
+
expect(user.readonly?).to be_truthy
|
|
589
|
+
expect(user.reload.readonly?).to be_truthy
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
it 'keeps an explicit readonly through a reload on a non-partial record' do
|
|
593
|
+
record.readonly!
|
|
594
|
+
expect(record.reload.readonly?).to be_truthy
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
context 'on automatic casting' do
|
|
599
|
+
before :each do
|
|
600
|
+
Activity.create!(title: 'Plain activity')
|
|
601
|
+
ActivityBook.create!(title: 'A book', url: 'bookurl1')
|
|
602
|
+
ActivityPost.create!(title: 'A post', url: 'posturl1')
|
|
603
|
+
ActivityPost::Sample.create!(title: 'A sample')
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it 'returns the correct class without any opt-in' do
|
|
607
|
+
expect(Activity.order(:id).load.map(&:class)).to \
|
|
608
|
+
eql([Activity, ActivityBook, ActivityPost, ActivityPost::Sample])
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
it 'marks casted records as partial and read-only' do
|
|
612
|
+
records = Activity.order(:id).load.to_a
|
|
613
|
+
|
|
614
|
+
expect(records[0].partial_record?).to be_falsey
|
|
615
|
+
expect(records[0].readonly?).to be_falsey
|
|
616
|
+
expect(records[1].partial_record?).to be_truthy
|
|
617
|
+
expect(records[1].readonly?).to be_truthy
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'raises when reading a column that was not loaded' do
|
|
621
|
+
book = Activity.order(:id).load.to_a[1]
|
|
622
|
+
expect { book.url }.to raise_error(ActiveModel::MissingAttributeError)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
it 'raises when persisting a partial record' do
|
|
626
|
+
book = Activity.order(:id).load.to_a[1]
|
|
627
|
+
expect { book.update!(title: 'Changed') }.to \
|
|
628
|
+
raise_error(ActiveRecord::ReadOnlyRecord)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
it 'destroys a partial record from both tables' do
|
|
632
|
+
book = Activity.order(:id).load.to_a[1]
|
|
633
|
+
|
|
634
|
+
expect(book.readonly?).to be_truthy
|
|
635
|
+
expect(book.destroy).to be_truthy
|
|
636
|
+
expect(Activity.where(id: book.id)).to be_empty
|
|
637
|
+
expect(ActivityBook.where(id: book.id)).to be_empty
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it 'keeps a partial record read-only after destroying it' do
|
|
641
|
+
book = Activity.order(:id).load.to_a[1]
|
|
642
|
+
book.destroy
|
|
643
|
+
|
|
644
|
+
expect(book.readonly?).to be_truthy
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
it 'destroys partial records through a dependent association' do
|
|
648
|
+
author = Author.create!(name: 'An author name')
|
|
649
|
+
book = ActivityBook.create!(title: 'Owned book', url: 'ownedurl', author: author)
|
|
650
|
+
author.destroy
|
|
651
|
+
|
|
652
|
+
expect(Activity.where(id: book.id)).to be_empty
|
|
653
|
+
expect(ActivityBook.where(id: book.id)).to be_empty
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
it 'does not mark a dependent that adds no columns as partial' do
|
|
657
|
+
sample = ActivityPost.order(:id).load.to_a.last
|
|
658
|
+
|
|
659
|
+
expect(sample).to be_instance_of(ActivityPost::Sample)
|
|
660
|
+
expect(sample.partial_record?).to be_falsey
|
|
661
|
+
expect(sample.readonly?).to be_falsey
|
|
662
|
+
expect(sample.update!(title: 'Changed')).to be_truthy
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
it 'loads the full record on reload' do
|
|
666
|
+
book = Activity.order(:id).load.to_a[1]
|
|
667
|
+
book.reload
|
|
668
|
+
|
|
669
|
+
expect(book.url).to eql('bookurl1')
|
|
670
|
+
expect(book.partial_record?).to be_falsey
|
|
671
|
+
expect(book.readonly?).to be_falsey
|
|
672
|
+
expect(book.changed?).to be_falsey
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
it 'does not expose the internal record class attribute' do
|
|
676
|
+
book = Activity.order(:id).load.to_a[1]
|
|
677
|
+
|
|
678
|
+
expect(book).to be_instance_of(ActivityBook)
|
|
679
|
+
expect(book).not_to respond_to(:_record_class)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
it 'does not expose the internal record class attribute on a non-casted record' do
|
|
683
|
+
plain = Activity.order(:id).load.first
|
|
684
|
+
|
|
685
|
+
expect(plain).to be_instance_of(Activity)
|
|
686
|
+
expect(plain).not_to respond_to(:_record_class)
|
|
687
|
+
expect(plain.attributes).not_to have_key('_record_class')
|
|
688
|
+
expect { Activity.new(plain.attributes) }.not_to raise_error
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
it 'does not affect single table inheritance' do
|
|
692
|
+
AuthorJournalist.create!(name: 'An author name')
|
|
693
|
+
expect(AuthorJournalist.first).to be_instance_of(AuthorJournalist)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it 'does not cast records loaded through an explicit select' do
|
|
697
|
+
book = ActivityBook.create!(title: 'Selected book', url: 'selurl')
|
|
698
|
+
record = nil
|
|
699
|
+
|
|
700
|
+
expect { record = Activity.select(:id, :title).where(id: book.id).first }.to \
|
|
701
|
+
output(/Activity .* omits :_regclass/).to_stderr
|
|
702
|
+
|
|
703
|
+
expect(record).to be_instance_of(Activity)
|
|
704
|
+
expect(record.partial_record?).to be_falsey
|
|
705
|
+
expect(record.readonly?).to be_falsey
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
it 'casts records when the select asks for the record class column' do
|
|
709
|
+
book = ActivityBook.create!(title: 'Selected book', url: 'selurl')
|
|
710
|
+
record = Activity.select(:_regclass, :id, :title).where(id: book.id).first
|
|
711
|
+
|
|
712
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
713
|
+
expect(record.partial_record?).to be_truthy
|
|
714
|
+
expect(record.readonly?).to be_truthy
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
it 'keeps extra selected columns on both casted and non-casted records' do
|
|
718
|
+
author = Author.create!(name: 'The author')
|
|
719
|
+
Activity.create!(title: 'Joined activity', author: author)
|
|
720
|
+
ActivityBook.create!(title: 'Joined book', url: 'jburl', author: author)
|
|
721
|
+
|
|
722
|
+
records = Activity.joins(:author).where(author_id: author.id)
|
|
723
|
+
.select(:_regclass, 'activities.*', 'authors.name as author_name')
|
|
724
|
+
.order(:id).to_a
|
|
725
|
+
|
|
726
|
+
expect(records[0]).to be_instance_of(Activity)
|
|
727
|
+
expect(records[1]).to be_instance_of(ActivityBook)
|
|
728
|
+
expect(records[0][:author_name]).to eql('The author')
|
|
729
|
+
expect(records[1][:author_name]).to eql('The author')
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
it 'keeps a user alias containing a double underscore on both plain and casted records' do
|
|
733
|
+
author = Author.create!(name: 'Underscore author')
|
|
734
|
+
Activity.create!(title: 'Underscore activity', author: author)
|
|
735
|
+
ActivityBook.create!(title: 'Underscore book', url: 'underscoreurl', author: author)
|
|
736
|
+
|
|
737
|
+
records = Activity.joins(:author).where(author_id: author.id)
|
|
738
|
+
.select(:_regclass, 'activities.*', 'authors.name as custom__alias')
|
|
739
|
+
.order(:id).to_a
|
|
740
|
+
|
|
741
|
+
expect(records[0]).to be_instance_of(Activity)
|
|
742
|
+
expect(records[1]).to be_instance_of(ActivityBook)
|
|
743
|
+
expect(records[0][:custom__alias]).to eql('Underscore author')
|
|
744
|
+
expect(records[1][:custom__alias]).to eql('Underscore author')
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
it 'warns when an explicit select fails to cast the loaded records' do
|
|
748
|
+
expect { Activity.select(:id, :title).to_a }.to \
|
|
749
|
+
output(/Activity .* omits :_regclass/).to_stderr
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
it 'does not warn when the record class column ends up selected' do
|
|
753
|
+
expect { Activity.select(:_regclass, :id).to_a }.not_to output.to_stderr
|
|
754
|
+
expect { Activity.select(:_regclass, :id).select(:title).to_a }.not_to output.to_stderr
|
|
755
|
+
expect { Activity.select(:id).select(:_regclass).to_a }.not_to output.to_stderr
|
|
756
|
+
expect { Activity.itself_only.select(:id).to_a }.not_to output.to_stderr
|
|
757
|
+
expect { AuthorJournalist.select(:id).to_a }.not_to output.to_stderr
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
it 'does not warn when a marker-bearing select returns only parent-table rows' do
|
|
761
|
+
plain = Activity.order(:id).first
|
|
762
|
+
|
|
763
|
+
expect { Activity.select(:_regclass, :id, :title).where(id: plain.id).to_a }.not_to \
|
|
764
|
+
output.to_stderr
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
it 'does not warn when the record class column is hand-written as raw SQL' do
|
|
768
|
+
plain = Activity.order(:id).first
|
|
769
|
+
|
|
770
|
+
expect {
|
|
771
|
+
Activity.select('tableoid::regclass AS _record_class', :id, :title).where(id: plain.id).to_a
|
|
772
|
+
}.not_to output.to_stderr
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
it 'does not warn about a relation only used to build a subquery' do
|
|
776
|
+
ActivityBook.create!(title: 'Sub author book', url: 'subauthorurl')
|
|
777
|
+
|
|
778
|
+
expect { Author.where(id: Activity.all) }.not_to output.to_stderr
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
it 'warns only once when an association scope is built more than once' do
|
|
782
|
+
Author.has_many :selected_activities, -> { select(:id, :title) },
|
|
783
|
+
class_name: 'Activity', foreign_key: :author_id
|
|
784
|
+
|
|
785
|
+
author = Author.create!(name: 'Repeated author')
|
|
786
|
+
ActivityBook.create!(title: 'Repeated book', url: 'repeaturl', author: author)
|
|
787
|
+
|
|
788
|
+
expect { author.selected_activities.to_a }.to \
|
|
789
|
+
output(/\A[^\n]*omits :_regclass[^\n]*\n\z/).to_stderr
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
it 'does not warn while building an auxiliary statement over an inheriting model' do
|
|
793
|
+
expect { Activity.select(:id, :title).arel }.not_to output.to_stderr
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
it 'translates the record class token into the marker' do
|
|
797
|
+
sql = Activity.select(:_regclass, :id).to_sql
|
|
798
|
+
|
|
799
|
+
expect(sql).to include('"activities"."tableoid"::regclass AS _record_class')
|
|
800
|
+
expect(sql).not_to include('"_regclass"')
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
it 'adds the record class exactly once through repeated chaining' do
|
|
804
|
+
relation = Activity.all
|
|
805
|
+
3.times do
|
|
806
|
+
relation = relation.where(active: nil)
|
|
807
|
+
relation.to_a
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
expect(relation.to_sql.scan('_record_class').size).to eql(1)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
it 'does not add the record class to a relation used as a subquery' do
|
|
814
|
+
author = Author.create!(name: 'Sub author')
|
|
815
|
+
ActivityBook.create!(title: 'Sub book', url: 'suburl', author: author)
|
|
816
|
+
|
|
817
|
+
expect(Author.where(id: Activity.select(:author_id)).to_a).to eql([author])
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
it 'does not add the record class to a grouped selection' do
|
|
821
|
+
expect(Activity.select(:kind).group(:kind).to_a.size).to eql(1)
|
|
822
|
+
expect(Activity.group(:kind).exists?).to be_truthy
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
it 'does not add the record class to a distinct selection' do
|
|
826
|
+
ActivityBook.create!(title: 'Plain activity', url: 'duperurl')
|
|
827
|
+
|
|
828
|
+
expect(Activity.distinct.select(:title).map(&:title)).to \
|
|
829
|
+
match_array(['Plain activity', 'A book', 'A post', 'A sample'])
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
it 'does not add the record class when reading from a subquery' do
|
|
833
|
+
records = Activity.from(Activity.itself_only, :activities).to_a
|
|
834
|
+
|
|
835
|
+
expect(records.size).to eql(1)
|
|
836
|
+
expect(records.first.title).to eql('Plain activity')
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
it 'does not add the record class to plucked columns' do
|
|
840
|
+
ActivityBook.create!(title: 'Plucked', url: 'plurl')
|
|
841
|
+
expect(Activity.pluck(:title)).to all(be_a(String))
|
|
842
|
+
expect(Activity.pluck(:id, :title).first.size).to eql(2)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
it 'does not add the record class to calculations' do
|
|
846
|
+
ActivityBook.create!(title: 'Counted', url: 'cnturl')
|
|
847
|
+
expect(Activity.count).to be_a(Integer)
|
|
848
|
+
expect(Activity.sum(:id)).to be_a(Integer)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
it 'does not break calculations combined with buckets' do
|
|
852
|
+
ActivityBook.create!(title: 'Bucketed', url: 'bkturl')
|
|
853
|
+
expect { Activity.buckets(:id, 0..50, count: 5).count }.not_to raise_error
|
|
854
|
+
expect { User.buckets(:age, 0..50, count: 5).count }.not_to raise_error
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
it 'still casts a relation that was previously used for a calculation' do
|
|
858
|
+
book = ActivityBook.create!(title: 'Reused', url: 'reurl')
|
|
859
|
+
relation = Activity.where(id: book.id)
|
|
860
|
+
relation.count
|
|
861
|
+
|
|
862
|
+
expect(relation.to_a.first).to be_instance_of(ActivityBook)
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
it 'does not add the record class when plucking an explicit selection' do
|
|
866
|
+
ActivityBook.create!(title: 'Selected pluck', url: 'spurl')
|
|
867
|
+
expect(Activity.select(:id, :title).pluck(:id).first).to be_a(Integer)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
context 'when a record class cannot be resolved' do
|
|
871
|
+
after { Activity.reset_column_information }
|
|
872
|
+
|
|
873
|
+
it 'raises pointing at the irregular models setting' do
|
|
874
|
+
Activity.instance_variable_set(:@casted_dependents, {})
|
|
875
|
+
|
|
876
|
+
expect { Activity.order(:id).load.to_a }.to \
|
|
877
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /activity_books/)
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
context 'using uuid' do
|
|
882
|
+
it 'returns the correct class' do
|
|
883
|
+
Question.create!(title: 'Simple question')
|
|
884
|
+
QuestionSelect.create!(title: 'Select question')
|
|
885
|
+
|
|
886
|
+
expect(Question.order(:created_at).load.map(&:class)).to \
|
|
887
|
+
eql([Question, QuestionSelect])
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
|
|
266
892
|
context 'on relation' do
|
|
267
893
|
let(:base) { Activity }
|
|
268
894
|
let(:child) { ActivityBook }
|
|
@@ -270,14 +896,20 @@ RSpec.describe 'TableInheritance' do
|
|
|
270
896
|
|
|
271
897
|
it 'has operation methods' do
|
|
272
898
|
expect(base).to respond_to(:itself_only)
|
|
273
|
-
expect(base).to respond_to(:
|
|
274
|
-
expect(base.new).to respond_to(:cast_record)
|
|
899
|
+
expect(base).to respond_to(:expand_records)
|
|
275
900
|
end
|
|
276
901
|
|
|
277
902
|
context 'itself only' do
|
|
278
|
-
it '
|
|
279
|
-
|
|
280
|
-
|
|
903
|
+
it 'adds the record class to queries on a table with dependents' do
|
|
904
|
+
result = 'SELECT "activities".*'
|
|
905
|
+
result << ', "activities"."tableoid"::regclass AS _record_class'
|
|
906
|
+
result << ' FROM "activities"'
|
|
907
|
+
expect(base.all.to_sql).to eql(result)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
it 'does not add the record class to a table without dependents' do
|
|
911
|
+
expect(other.all.to_sql).to \
|
|
912
|
+
eql("SELECT \"authors\".* FROM \"authors\" WHERE \"authors\".\"type\" = 'AuthorJournalist'")
|
|
281
913
|
end
|
|
282
914
|
|
|
283
915
|
it 'adds the only condition to the query' do
|
|
@@ -295,7 +927,7 @@ RSpec.describe 'TableInheritance' do
|
|
|
295
927
|
end
|
|
296
928
|
end
|
|
297
929
|
|
|
298
|
-
context '
|
|
930
|
+
context 'expand records' do
|
|
299
931
|
before :each do
|
|
300
932
|
base.create(title: 'Activity test')
|
|
301
933
|
child.create(title: 'Activity book', url: 'bookurl1')
|
|
@@ -313,31 +945,28 @@ RSpec.describe 'TableInheritance' do
|
|
|
313
945
|
result << ', COALESCE("i_0"."url", "i_1"."url", "i_2"."url") AS url, "i_0"."activated" AS activity_books__activated'
|
|
314
946
|
result << ', "i_1"."activated" AS activity_posts__activated, "i_2"."activated" AS activity_post_samples__activated'
|
|
315
947
|
result << ', COALESCE("i_1"."file", "i_2"."file") AS file, COALESCE("i_1"."post_id", "i_2"."post_id") AS post_id'
|
|
316
|
-
result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books', 'activity_posts', 'activity_post_samples') AS _auto_cast"
|
|
317
948
|
result << ' FROM "activities"'
|
|
318
949
|
result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
|
|
319
950
|
result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
|
|
320
951
|
result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
|
|
321
|
-
expect(base.
|
|
952
|
+
expect(base.expand_records(eager_load: true).all.to_sql).to eql(result)
|
|
322
953
|
end
|
|
323
954
|
|
|
324
955
|
it 'can be have simplefied joins' do
|
|
325
956
|
result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
|
|
326
957
|
result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
|
|
327
|
-
result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
|
|
328
958
|
result << ' FROM "activities"'
|
|
329
959
|
result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
|
|
330
|
-
expect(base.
|
|
960
|
+
expect(base.expand_records(child, eager_load: true).all.to_sql).to eql(result)
|
|
331
961
|
end
|
|
332
962
|
|
|
333
963
|
it 'can be filtered by record type' do
|
|
334
964
|
result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
|
|
335
965
|
result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
|
|
336
|
-
result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
|
|
337
966
|
result << ' FROM "activities"'
|
|
338
967
|
result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
|
|
339
968
|
result << " WHERE \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books')"
|
|
340
|
-
expect(base.
|
|
969
|
+
expect(base.expand_records(child, filter: true, eager_load: true).all.to_sql).to eql(result)
|
|
341
970
|
end
|
|
342
971
|
|
|
343
972
|
it 'works with count and does not add extra columns' do
|
|
@@ -346,7 +975,7 @@ RSpec.describe 'TableInheritance' do
|
|
|
346
975
|
result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
|
|
347
976
|
result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
|
|
348
977
|
result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
|
|
349
|
-
query = get_last_executed_query{ base.
|
|
978
|
+
query = get_last_executed_query{ base.expand_records(eager_load: true).all.count }
|
|
350
979
|
expect(query).to eql(result)
|
|
351
980
|
end
|
|
352
981
|
|
|
@@ -356,14 +985,34 @@ RSpec.describe 'TableInheritance' do
|
|
|
356
985
|
result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
|
|
357
986
|
result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
|
|
358
987
|
result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
|
|
359
|
-
query = get_last_executed_query{ base.
|
|
988
|
+
query = get_last_executed_query{ base.expand_records(eager_load: true).all.sum(:id) }
|
|
360
989
|
expect(query).to eql(result)
|
|
361
990
|
end
|
|
362
991
|
|
|
992
|
+
it 'does not accumulate the extra columns through repeated chaining' do
|
|
993
|
+
relation = base.expand_records(eager_load: true)
|
|
994
|
+
3.times do
|
|
995
|
+
relation = relation.where(active: nil)
|
|
996
|
+
relation.to_a
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
sql = relation.to_sql
|
|
1000
|
+
expect(sql.scan('_record_class').size).to eql(1)
|
|
1001
|
+
expect(sql.scan('"i_0"."description"').size).to eql(1)
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
it 'filters by every expandable dependent when no type is given' do
|
|
1005
|
+
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
1006
|
+
records = base.expand_records(filter: true).order(:id).to_a
|
|
1007
|
+
|
|
1008
|
+
expect(records.map(&:class)).to eql([ActivityBook, ActivityPost])
|
|
1009
|
+
expect(records.map(&:url)).to eql(['bookurl1', 'posturl1'])
|
|
1010
|
+
end
|
|
1011
|
+
|
|
363
1012
|
it 'returns the correct model object' do
|
|
364
1013
|
ActivityPost.create(title: 'Activity post')
|
|
365
1014
|
ActivityPost::Sample.create(title: 'Activity post')
|
|
366
|
-
records = base.
|
|
1015
|
+
records = base.expand_records(eager_load: true).order(:id).load.to_a
|
|
367
1016
|
|
|
368
1017
|
expect(records[0]).to be_instance_of(Activity)
|
|
369
1018
|
expect(records[1]).to be_instance_of(ActivityBook)
|
|
@@ -371,81 +1020,403 @@ RSpec.describe 'TableInheritance' do
|
|
|
371
1020
|
expect(records[3]).to be_instance_of(ActivityPost::Sample)
|
|
372
1021
|
end
|
|
373
1022
|
|
|
374
|
-
it '
|
|
1023
|
+
it 'only fully loads the requested records' do
|
|
375
1024
|
ActivityPost.create(title: 'Activity post')
|
|
376
|
-
records = base.
|
|
1025
|
+
records = base.expand_records(ActivityBook, eager_load: true).order(:id).load.to_a
|
|
377
1026
|
|
|
378
|
-
expect(records[0]).to be_instance_of(Activity)
|
|
379
1027
|
expect(records[1]).to be_instance_of(ActivityBook)
|
|
380
|
-
expect(records[
|
|
1028
|
+
expect(records[1].partial_record?).to be_falsey
|
|
1029
|
+
expect(records[2]).to be_instance_of(ActivityPost)
|
|
1030
|
+
expect(records[2].partial_record?).to be_truthy
|
|
381
1031
|
end
|
|
382
1032
|
|
|
383
1033
|
it 'correctly identifies same name attributes' do
|
|
384
1034
|
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
385
|
-
records = base.
|
|
1035
|
+
records = base.expand_records(eager_load: true).order(:id).load.to_a
|
|
386
1036
|
|
|
387
1037
|
expect(records[1].url).to eql('bookurl1')
|
|
388
1038
|
expect(records[2].url).to eql('posturl1')
|
|
389
1039
|
end
|
|
390
1040
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
record = base.cast_records.order(:id).load.last
|
|
1041
|
+
it 'does not make internal inheritance attributes accessible' do
|
|
1042
|
+
record = base.expand_records(eager_load: true).order(:id).load.last
|
|
394
1043
|
|
|
395
1044
|
expect(record).to be_instance_of(ActivityBook)
|
|
396
1045
|
expect(record).not_to respond_to(:_record_class)
|
|
397
|
-
expect(record).not_to respond_to(:_auto_cast)
|
|
398
1046
|
end
|
|
399
|
-
end
|
|
400
1047
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
base.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1048
|
+
it 'drops sibling dependent columns from a casted record' do
|
|
1049
|
+
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
1050
|
+
record = base.expand_records(eager_load: true).order(:id).load[1]
|
|
1051
|
+
|
|
1052
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
1053
|
+
expect(record.url).to eql('bookurl1')
|
|
1054
|
+
expect(record.attributes).not_to have_key('file')
|
|
1055
|
+
expect(record.attributes).not_to have_key('post_id')
|
|
1056
|
+
expect { ActivityBook.new(record.attributes) }.not_to raise_error
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
it 'drops a genuine dependent-prefixed column from a casted record' do
|
|
1060
|
+
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
1061
|
+
record = base.expand_records(eager_load: true).order(:id).load[1]
|
|
1062
|
+
|
|
1063
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
1064
|
+
expect(record.attributes).not_to have_key('activity_posts__activated')
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
it 'drops sibling dependent columns from a non-casted parent record' do
|
|
1068
|
+
record = base.expand_records(eager_load: true).order(:id).load[0]
|
|
1069
|
+
|
|
1070
|
+
expect(record).to be_instance_of(Activity)
|
|
1071
|
+
expect(record.attributes).not_to have_key('description')
|
|
1072
|
+
expect(record.attributes).not_to have_key('url')
|
|
1073
|
+
expect { Activity.new(record.attributes) }.not_to raise_error
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
it 'defaults to every dependent that adds columns' do
|
|
1077
|
+
relation = base.expand_records
|
|
1078
|
+
expect(relation.expand_records_values).to \
|
|
1079
|
+
eql(base.inheritance_expandable_dependents.values)
|
|
407
1080
|
end
|
|
408
1081
|
|
|
409
|
-
it '
|
|
410
|
-
|
|
411
|
-
expect(
|
|
412
|
-
expect(other.first.cast_record).to be_a(other)
|
|
1082
|
+
it 'limits the expansion to the given models' do
|
|
1083
|
+
relation = base.expand_records(child)
|
|
1084
|
+
expect(relation.expand_records_values).to eql([child])
|
|
413
1085
|
end
|
|
414
1086
|
|
|
415
|
-
it '
|
|
416
|
-
|
|
417
|
-
|
|
1087
|
+
it 'does not add joins without eager loading' do
|
|
1088
|
+
result = 'SELECT "activities".*'
|
|
1089
|
+
result << ', "activities"."tableoid"::regclass AS _record_class'
|
|
1090
|
+
result << ' FROM "activities"'
|
|
1091
|
+
expect(base.expand_records.all.to_sql).to eql(result)
|
|
418
1092
|
end
|
|
419
1093
|
|
|
420
|
-
it '
|
|
421
|
-
expect
|
|
422
|
-
|
|
423
|
-
expect
|
|
1094
|
+
it 'cannot be combined with itself only' do
|
|
1095
|
+
expect { base.itself_only.expand_records }.to \
|
|
1096
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /itself_only/)
|
|
1097
|
+
expect { base.expand_records.itself_only }.to \
|
|
1098
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /itself_only/)
|
|
424
1099
|
end
|
|
425
1100
|
|
|
426
|
-
it '
|
|
427
|
-
expect(base.
|
|
1101
|
+
it 'cannot be merged with itself only' do
|
|
1102
|
+
expect { base.expand_records.merge(base.itself_only) }.to \
|
|
1103
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /itself_only/)
|
|
1104
|
+
expect { base.itself_only.merge(base.expand_records) }.to \
|
|
1105
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /itself_only/)
|
|
428
1106
|
end
|
|
429
1107
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
1108
|
+
it 'loads the missing columns with one query per table' do
|
|
1109
|
+
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
1110
|
+
records = nil
|
|
433
1111
|
|
|
434
|
-
|
|
435
|
-
base.
|
|
436
|
-
child.create(title: 'Select question')
|
|
437
|
-
base.instance_variable_set(:@casted_dependents, nil)
|
|
1112
|
+
queries = capture_executed_queries do
|
|
1113
|
+
records = base.expand_records.order(:id).load.to_a
|
|
438
1114
|
end
|
|
439
1115
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1116
|
+
expect(queries.size).to eql(3)
|
|
1117
|
+
expect(records[1]).to be_instance_of(ActivityBook)
|
|
1118
|
+
expect(records[1].url).to eql('bookurl1')
|
|
1119
|
+
expect(records[2]).to be_instance_of(ActivityPost)
|
|
1120
|
+
expect(records[2].url).to eql('posturl1')
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
it 'produces complete and writable records' do
|
|
1124
|
+
record = base.expand_records.order(:id).load.to_a[1]
|
|
1125
|
+
|
|
1126
|
+
expect(record.partial_record?).to be_falsey
|
|
1127
|
+
expect(record.readonly?).to be_falsey
|
|
1128
|
+
expect(record.update!(title: 'Changed')).to be_truthy
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
it 'does not leave expanded records dirty' do
|
|
1132
|
+
record = base.expand_records.order(:id).load.to_a[1]
|
|
1133
|
+
|
|
1134
|
+
expect(record.changed?).to be_falsey
|
|
1135
|
+
expect(record.changes).to be_empty
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
it 'only selects the primary key and the extra columns' do
|
|
1139
|
+
queries = capture_executed_queries do
|
|
1140
|
+
base.expand_records(child).order(:id).load.to_a
|
|
443
1141
|
end
|
|
444
1142
|
|
|
445
|
-
|
|
446
|
-
|
|
1143
|
+
expansion = queries.find { |sql| sql.include?('activity_books') }
|
|
1144
|
+
expect(expansion).to match(/SELECT "activity_books"\."id"/)
|
|
1145
|
+
expect(expansion).to include('"description"')
|
|
1146
|
+
expect(expansion).to include('"url"')
|
|
1147
|
+
expect(expansion).not_to include('"title"')
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
it 'reads the expansion from only the target table' do
|
|
1151
|
+
ActivityPost.create(title: 'Activity post', url: 'posturl1')
|
|
1152
|
+
|
|
1153
|
+
queries = capture_executed_queries do
|
|
1154
|
+
base.expand_records(ActivityPost).order(:id).load.to_a
|
|
447
1155
|
end
|
|
1156
|
+
|
|
1157
|
+
expansion = queries.find { |sql| sql.include?('activity_posts') }
|
|
1158
|
+
expect(expansion).to include('FROM ONLY "activity_posts"')
|
|
1159
|
+
expect(expansion).not_to include('_record_class')
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
it 'leaves records partial when the inherited row is gone' do
|
|
1163
|
+
vanished = child.create(title: 'Vanished book', url: 'bookurl2')
|
|
1164
|
+
covered = child.order(:id).first
|
|
1165
|
+
row = { 'description' => nil, 'url' => covered.url, 'activated' => nil }
|
|
1166
|
+
only_covered = ->(*) { { covered.id => row } }
|
|
1167
|
+
|
|
1168
|
+
allow_any_instance_of(Torque::PostgreSQL::Inheritance::Expander).to \
|
|
1169
|
+
receive(:fetch, &only_covered)
|
|
1170
|
+
|
|
1171
|
+
records = base.expand_records.order(:id).load.to_a.last(2)
|
|
1172
|
+
|
|
1173
|
+
expect(records.first.id).to eql(covered.id)
|
|
1174
|
+
expect(records.first.partial_record?).to be_falsey
|
|
1175
|
+
expect(records.first.url).to eql('bookurl1')
|
|
1176
|
+
|
|
1177
|
+
expect(records.last.id).to eql(vanished.id)
|
|
1178
|
+
expect(records.last.partial_record?).to be_truthy
|
|
1179
|
+
expect { records.last.url }.to raise_error(ActiveModel::MissingAttributeError)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
it 'preserves an explicit readonly through expansion' do
|
|
1183
|
+
record = base.readonly.expand_records.order(:id).load.to_a[1]
|
|
1184
|
+
|
|
1185
|
+
expect(record.url).to eql('bookurl1')
|
|
1186
|
+
expect(record.readonly?).to be_truthy
|
|
1187
|
+
expect { record.update!(title: 'Changed') }.to \
|
|
1188
|
+
raise_error(ActiveRecord::ReadOnlyRecord)
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
context 'on associations' do
|
|
1194
|
+
let(:author) { Author.create!(name: 'An author name') }
|
|
1195
|
+
|
|
1196
|
+
before :each do
|
|
1197
|
+
Activity.create!(title: 'Plain activity', author: author)
|
|
1198
|
+
ActivityBook.create!(title: 'A book', url: 'bookurl1', author: author)
|
|
1199
|
+
ActivityPost.create!(title: 'A post', url: 'posturl1', author: author)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
it 'returns the correct classes through a preloaded association' do
|
|
1203
|
+
activities = Author.includes(:activities).first.activities.sort_by(&:id)
|
|
1204
|
+
|
|
1205
|
+
expect(activities.map(&:class)).to \
|
|
1206
|
+
eql([Activity, ActivityBook, ActivityPost])
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
it 'leaves preloaded records partial without expanding' do
|
|
1210
|
+
activities = Author.includes(:activities).first.activities.sort_by(&:id)
|
|
1211
|
+
|
|
1212
|
+
expect(activities[1].partial_record?).to be_truthy
|
|
1213
|
+
expect { activities[1].url }.to raise_error(ActiveModel::MissingAttributeError)
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
it 'expands preloaded records when merged' do
|
|
1217
|
+
relation = Author.includes(:activities).merge(Activity.expand_records)
|
|
1218
|
+
activities = relation.first.activities.sort_by(&:id)
|
|
1219
|
+
|
|
1220
|
+
expect(activities[1]).to be_instance_of(ActivityBook)
|
|
1221
|
+
expect(activities[1].url).to eql('bookurl1')
|
|
1222
|
+
expect(activities[1].partial_record?).to be_falsey
|
|
1223
|
+
expect(activities[1].changed?).to be_falsey
|
|
1224
|
+
expect(activities[2].url).to eql('posturl1')
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
it 'uses one query per table when expanding through an association' do
|
|
1228
|
+
other_author = Author.create!(name: 'Another author name')
|
|
1229
|
+
Activity.create!(title: 'Other activity', author: other_author)
|
|
1230
|
+
ActivityBook.create!(title: 'Other book', url: 'bookurl2', author: other_author)
|
|
1231
|
+
ActivityPost.create!(title: 'Other post', url: 'posturl2', author: other_author)
|
|
1232
|
+
|
|
1233
|
+
queries = capture_executed_queries do
|
|
1234
|
+
Author.includes(:activities).merge(Activity.expand_records).load.to_a
|
|
448
1235
|
end
|
|
1236
|
+
|
|
1237
|
+
expect(queries.size).to eql(4)
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
it 'accumulates targets across repeated cross-model merges' do
|
|
1241
|
+
relation = Author.includes(:activities)
|
|
1242
|
+
.merge(Activity.expand_records(ActivityBook))
|
|
1243
|
+
.merge(Activity.expand_records(ActivityPost))
|
|
1244
|
+
|
|
1245
|
+
expect(relation.expand_records_scoped_value[Activity]).to \
|
|
1246
|
+
contain_exactly(ActivityBook, ActivityPost)
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
it 'refuses to merge an eager loaded expansion from another model' do
|
|
1250
|
+
expect { Author.includes(:activities).merge(Activity.expand_records(eager_load: true)) }.to \
|
|
1251
|
+
raise_error(Torque::PostgreSQL::InheritanceError, /eager load/)
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
it 'does not raise when a polymorphic association is mixed into the includes' do
|
|
1255
|
+
user = User.create!(name: 'A user')
|
|
1256
|
+
Comment.create!(user: user, content: 'A comment')
|
|
1257
|
+
|
|
1258
|
+
expect do
|
|
1259
|
+
Comment.includes(:commentable).merge(Activity.expand_records).load.to_a
|
|
1260
|
+
end.not_to raise_error
|
|
1261
|
+
end
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
context 'on eager loading' do
|
|
1265
|
+
let(:author) { Author.create!(name: 'An author name') }
|
|
1266
|
+
|
|
1267
|
+
let(:post_record) { Post.create!(title: 'A post record', author: author) }
|
|
1268
|
+
|
|
1269
|
+
before :each do
|
|
1270
|
+
Activity.create!(title: 'Plain activity', author: author)
|
|
1271
|
+
ActivityBook.create!(title: 'A book', url: 'bookurl1', author: author)
|
|
1272
|
+
ActivityPost.create!(title: 'A post', url: 'posturl1', author: author, post: post_record)
|
|
1273
|
+
ActivityPost::Sample.create!(title: 'A sample', url: 'sampleurl1', author: author)
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
it 'casts records the same way includes does' do
|
|
1277
|
+
eager = Activity.eager_load(:author).order(:id).to_a
|
|
1278
|
+
included = Activity.includes(:author).order(:id).to_a
|
|
1279
|
+
|
|
1280
|
+
expect(eager.map(&:class)).to eql(included.map(&:class))
|
|
1281
|
+
expect(eager.map(&:class)).to \
|
|
1282
|
+
eql([Activity, ActivityBook, ActivityPost, ActivityPost::Sample])
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
it 'leaves eager loaded records partial' do
|
|
1286
|
+
record = Activity.eager_load(:author).order(:id).to_a[1]
|
|
1287
|
+
|
|
1288
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
1289
|
+
expect(record.partial_record?).to be_truthy
|
|
1290
|
+
expect(record.readonly?).to be_truthy
|
|
1291
|
+
expect { record.url }.to raise_error(ActiveModel::MissingAttributeError)
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
it 'still loads the association without an extra query' do
|
|
1295
|
+
record = Activity.eager_load(:author).order(:id).first
|
|
1296
|
+
|
|
1297
|
+
queries = capture_executed_queries { record.author }
|
|
1298
|
+
expect(queries).to be_empty
|
|
1299
|
+
expect(record.author).to eql(author)
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
it 'loads a nested eager load without disturbing the aliasing' do
|
|
1303
|
+
queries = capture_executed_queries do
|
|
1304
|
+
record = Activity.eager_load(author: :posts).order(:id).load.first
|
|
1305
|
+
|
|
1306
|
+
expect(record).to be_instance_of(Activity)
|
|
1307
|
+
expect(record.author).to eql(author)
|
|
1308
|
+
expect(record.author.posts.map(&:title)).to eql(['A post record'])
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
expect(queries.size).to eql(1)
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
it 'loads multiple eager loads without disturbing the aliasing' do
|
|
1315
|
+
queries = capture_executed_queries do
|
|
1316
|
+
record = ActivityPost.eager_load(:author, :post).order(:id).first
|
|
1317
|
+
|
|
1318
|
+
expect(record).to be_instance_of(ActivityPost)
|
|
1319
|
+
expect(record.author).to eql(author)
|
|
1320
|
+
expect(record.post.title).to eql('A post record')
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
expect(queries.size).to eql(1)
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
it 'does not add the marker when the user provides an explicit select' do
|
|
1327
|
+
sql = Activity.eager_load(:author).select(:id, :title).to_sql
|
|
1328
|
+
expect(sql).not_to include('tableoid')
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
it 'does not add the marker when restricted to itself_only' do
|
|
1332
|
+
sql = Activity.itself_only.eager_load(:author).to_sql
|
|
1333
|
+
expect(sql).not_to include('tableoid')
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
it 'does not add the root marker when the root model does not physically inherit' do
|
|
1337
|
+
sql = Author.eager_load(:activities).to_sql
|
|
1338
|
+
expect(sql).not_to include('_record_class')
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
it 'casts an inheriting model loaded as a joined part' do
|
|
1342
|
+
records = Author.eager_load(:activities).first.activities.sort_by(&:id)
|
|
1343
|
+
|
|
1344
|
+
expect(records.map(&:class)).to \
|
|
1345
|
+
eql([Activity, ActivityBook, ActivityPost, ActivityPost::Sample])
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
it 'casts a joined part the same way includes does' do
|
|
1349
|
+
eager = Author.eager_load(:activities).first.activities.sort_by(&:id)
|
|
1350
|
+
included = Author.includes(:activities).first.activities.sort_by(&:id)
|
|
1351
|
+
|
|
1352
|
+
expect(eager.map(&:class)).to eql(included.map(&:class))
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
it 'casts a joined part without any extra query' do
|
|
1356
|
+
queries = capture_executed_queries do
|
|
1357
|
+
records = Author.eager_load(:activities).to_a.first.activities
|
|
1358
|
+
|
|
1359
|
+
expect(records.map(&:class)).to include(ActivityBook)
|
|
1360
|
+
end
|
|
1361
|
+
|
|
1362
|
+
expect(queries.size).to eql(1)
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
it 'leaves a casted joined part partial and read-only' do
|
|
1366
|
+
record = Author.eager_load(:activities).first.activities.sort_by(&:id)[1]
|
|
1367
|
+
|
|
1368
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
1369
|
+
expect(record.partial_record?).to be_truthy
|
|
1370
|
+
expect(record.readonly?).to be_truthy
|
|
1371
|
+
expect { record.url }.to raise_error(ActiveModel::MissingAttributeError)
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
it 'aliases the joined marker using the pattern of the other columns' do
|
|
1375
|
+
sql = Author.eager_load(:activities).to_sql
|
|
1376
|
+
|
|
1377
|
+
expect(sql).to match(/"activities"\."tableoid"::regclass AS t1_r\d+/)
|
|
1378
|
+
expect(sql.scan('tableoid').size).to eql(1)
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
it 'casts both ends when the root inherits as well' do
|
|
1382
|
+
record = Activity.eager_load(author: :activities).order(:id).to_a[1]
|
|
1383
|
+
|
|
1384
|
+
expect(record).to be_instance_of(ActivityBook)
|
|
1385
|
+
expect(record.author.activities.sort_by(&:id).map(&:class)).to \
|
|
1386
|
+
eql([Activity, ActivityBook, ActivityPost, ActivityPost::Sample])
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
it 'keeps the joined marker stable when the relation is built twice' do
|
|
1390
|
+
relation = Author.eager_load(:activities)
|
|
1391
|
+
relation.to_sql
|
|
1392
|
+
|
|
1393
|
+
expect(relation.to_sql.scan('tableoid').size).to eql(1)
|
|
1394
|
+
expect(relation.first.activities.map(&:class)).to include(ActivityBook)
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
it 'does not add the marker when the relation has an explicit from clause' do
|
|
1398
|
+
sql = Activity.from(Activity.itself_only, :activities).eager_load(:author).to_sql
|
|
1399
|
+
expect(sql).not_to include('tableoid')
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
it 'adds the marker exactly once to the generated sql' do
|
|
1403
|
+
sql = Activity.eager_load(:author).to_sql
|
|
1404
|
+
|
|
1405
|
+
expect(sql.scan('_record_class').size).to eql(1)
|
|
1406
|
+
expect(sql.scan('tableoid').size).to eql(1)
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1409
|
+
it 'casts and fully expands when merged with expand_records, with no extra author query' do
|
|
1410
|
+
relation = Activity.eager_load(:author).merge(Activity.expand_records)
|
|
1411
|
+
records = relation.order(:id).to_a
|
|
1412
|
+
|
|
1413
|
+
expect(records.map(&:class)).to \
|
|
1414
|
+
eql([Activity, ActivityBook, ActivityPost, ActivityPost::Sample])
|
|
1415
|
+
expect(records.map(&:partial_record?)).to eql([false, false, false, false])
|
|
1416
|
+
expect(records[1].url).to eql('bookurl1')
|
|
1417
|
+
|
|
1418
|
+
queries = capture_executed_queries { records.each(&:author) }
|
|
1419
|
+
expect(queries).to be_empty
|
|
449
1420
|
end
|
|
450
1421
|
end
|
|
451
1422
|
end
|