declare_schema 3.0.0 → 3.1.0.colin.2
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/.github/workflows/pipeline.yml +1 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +151 -105
- data/config/brakeman.ignore +5 -5
- data/declare_schema.gemspec +1 -1
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +0 -1
- data/lib/declare_schema/model/field_spec.rb +13 -5
- data/lib/declare_schema/model/habtm_model_shim.rb +46 -12
- data/lib/declare_schema/model.rb +114 -36
- data/lib/declare_schema/version.rb +1 -1
- data/lib/declare_schema.rb +10 -6
- data/lib/generators/declare_schema/migration/migrator.rb +23 -41
- data/spec/lib/declare_schema/field_spec_spec.rb +32 -0
- data/spec/lib/declare_schema/migration_generator_spec.rb +134 -0
- data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +13 -7
- data/spec/lib/generators/declare_schema/migration/migrator_default_pk_type_spec.rb +54 -0
- metadata +6 -7
- data/lib/declare_schema/field_declaration_dsl.rb +0 -37
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +0 -34
data/lib/declare_schema/model.rb
CHANGED
|
@@ -100,6 +100,7 @@ module DeclareSchema
|
|
|
100
100
|
_add_validations_for_field(name, type, args, options)
|
|
101
101
|
_add_index_for_field(name, args, **options)
|
|
102
102
|
_add_scopes_for_field(name, type, **options)
|
|
103
|
+
name.to_s == _declared_primary_key and raise ArgumentError, "no need to declare a field spec for the primary key #{name.inspect}"
|
|
103
104
|
field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, position: field_specs.size, **options)
|
|
104
105
|
attr_order << name unless attr_order.include?(name)
|
|
105
106
|
end
|
|
@@ -201,62 +202,105 @@ module DeclareSchema
|
|
|
201
202
|
super
|
|
202
203
|
|
|
203
204
|
reflection = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
|
|
204
|
-
|
|
205
|
-
foreign_key_column_options = column_options.dup
|
|
206
|
-
|
|
207
|
-
# Note: the foreign key limit: should match the primary key limit:. (If there is a foreign key constraint,
|
|
208
|
-
# those limits _must_ match.) We'd like to call _infer_fk_limit and get the limit right from the PK.
|
|
209
|
-
# But we can't here, because that will mess up the autoloader to follow every belongs_to association right
|
|
210
|
-
# when it is declared. So instead we assume :bigint (integer limit: 8) below, while also registering this
|
|
211
|
-
# pre_migration: callback to double-check that assumption Just In Time--right before we generate a migration.
|
|
212
|
-
#
|
|
213
|
-
# The one downside of this approach is that application code that asks the field_spec for the declared
|
|
214
|
-
# foreign key limit: will always get 8 back even if this is a grandfathered foreign key that points to
|
|
215
|
-
# a limit: 4 primary key. It seems unlikely that any application code would do this.
|
|
216
|
-
foreign_key_column_options[:pre_migration] = ->(field_spec) do
|
|
217
|
-
if (inferred_limit = _infer_fk_limit(foreign_key_column, reflection))
|
|
218
|
-
field_spec.sql_options[:limit] = inferred_limit
|
|
219
|
-
end
|
|
220
|
-
end
|
|
205
|
+
foreign_key_column_name = reflection.foreign_key or raise "Couldn't find foreign_key for #{name} in #{reflection.inspect}"
|
|
221
206
|
|
|
222
|
-
|
|
207
|
+
field_specs[foreign_key_column_name] = _infer_foreign_key_field_spec(foreign_key_column_name, reflection, column_options)
|
|
223
208
|
|
|
224
209
|
if reflection.options[:polymorphic]
|
|
225
210
|
foreign_type = options[:foreign_type] || "#{name}_type"
|
|
226
211
|
_declare_polymorphic_type_field(foreign_type, column_options)
|
|
227
212
|
if ::DeclareSchema.default_generate_indexing && index_options
|
|
228
|
-
index([foreign_type,
|
|
213
|
+
index([foreign_type, foreign_key_column_name], **index_options)
|
|
229
214
|
end
|
|
230
215
|
else
|
|
231
216
|
if ::DeclareSchema.default_generate_indexing && index_options
|
|
232
|
-
index([
|
|
217
|
+
index([foreign_key_column_name], **index_options)
|
|
233
218
|
end
|
|
234
219
|
|
|
235
220
|
if ::DeclareSchema.default_generate_foreign_keys && constraint_name != false
|
|
236
|
-
constraint(
|
|
221
|
+
constraint(foreign_key_column_name,
|
|
222
|
+
constraint_name: constraint_name || index_options&.[](:name),
|
|
223
|
+
parent_class_name: reflection.class_name,
|
|
224
|
+
dependent: dependent_delete)
|
|
237
225
|
end
|
|
238
226
|
end
|
|
239
227
|
end
|
|
240
228
|
|
|
241
|
-
|
|
229
|
+
# Returns a FieldSpec for the foreign key column of a belongs_to association.
|
|
230
|
+
# - For a polymorphic association, the FK uses `DeclareSchema.default_generated_primary_key_type`
|
|
231
|
+
# (mirroring `config.generators.primary_key_type`, default :bigint), or :integer with the
|
|
232
|
+
# existing column's limit if the column already exists in the database.
|
|
233
|
+
# - For a non-polymorphic association, the FK should mirror the primary key it points
|
|
234
|
+
# at (same data type, same options like limit:, charset:, etc.). However we cannot
|
|
235
|
+
# load the parent model right now (at `belongs_to` time) without risking dependency
|
|
236
|
+
# cycles between models, so we install a `resolver:` callback. The migration
|
|
237
|
+
# generator calls that resolver at generation time -- after all models are
|
|
238
|
+
# eager-loaded -- and the resolver returns a fully-mirrored FieldSpec that the
|
|
239
|
+
# generator swaps in for this default_spec.
|
|
240
|
+
def _infer_foreign_key_field_spec(foreign_key_column_name, reflection, column_options)
|
|
242
241
|
if reflection.options[:polymorphic]
|
|
243
|
-
if (foreign_key_column = _column(
|
|
244
|
-
|
|
242
|
+
if (foreign_key_column = _column(foreign_key_column_name)) && foreign_key_column.type == :integer
|
|
243
|
+
# grandfather foreign key column to match what's in the database
|
|
244
|
+
column_options = column_options.merge(limit: foreign_key_column.limit)
|
|
245
245
|
end
|
|
246
|
+
FieldSpec.new(self, foreign_key_column_name, DeclareSchema.default_generated_primary_key_type, position: field_specs.size, **column_options)
|
|
246
247
|
else
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
else
|
|
253
|
-
if klass.table_exists? && (pk_column = klass.columns_hash[klass._declared_primary_key])
|
|
254
|
-
pk_id_type = pk_column.type
|
|
255
|
-
if pk_id_type == :integer
|
|
256
|
-
pk_column.limit
|
|
257
|
-
end
|
|
258
|
-
end
|
|
248
|
+
# Capture only what we need from `reflection` (no `reflection.klass` here -- that
|
|
249
|
+
# would force the parent model to load, which is exactly the cycle we are avoiding).
|
|
250
|
+
# `reflection.klass` is resolved lazily inside the block below.
|
|
251
|
+
resolver = ->(default_spec) do
|
|
252
|
+
_resolve_belongs_to_foreign_key_field_spec(reflection, default_spec)
|
|
259
253
|
end
|
|
254
|
+
FieldSpec.new(self, foreign_key_column_name, DeclareSchema.default_generated_primary_key_type,
|
|
255
|
+
position: field_specs.size, resolver:, **column_options)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Called at migration generation time to mirror the parent model's primary key.
|
|
260
|
+
# Always returns a FieldSpec: the default_spec unchanged when the parent class is not
|
|
261
|
+
# a declare_schema model (we can't ask for its PK spec, so the configured default PK
|
|
262
|
+
# type is the best we can offer without inspecting the DB), otherwise a fully mirrored
|
|
263
|
+
# FieldSpec.
|
|
264
|
+
#
|
|
265
|
+
# Reconciliation with the live DB: if the parent's PK column already exists in the
|
|
266
|
+
# database with the same Rails type but a different :limit (e.g. a legacy table where
|
|
267
|
+
# `id` is INT(4) but the model now declares the default :bigint), prefer the live
|
|
268
|
+
# column's :limit so the FK matches what's actually on disk. This preserves the
|
|
269
|
+
# behavior of the old DB-column lookup (formerly `fk_field_options`) without
|
|
270
|
+
# overriding intentional type changes.
|
|
271
|
+
def _resolve_belongs_to_foreign_key_field_spec(reflection, default_spec)
|
|
272
|
+
klass = reflection.klass or
|
|
273
|
+
raise "Couldn't find belongs_to klass for #{reflection.name} on #{name} in #{reflection.inspect}"
|
|
274
|
+
|
|
275
|
+
if klass.respond_to?(:_primary_key_field_spec)
|
|
276
|
+
_mirror_parent_primary_key(klass, default_spec)
|
|
277
|
+
else
|
|
278
|
+
default_spec
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Build a FieldSpec for the FK by mirroring the parent's declared primary key,
|
|
283
|
+
# then reconciling against the live DB column when it differs only in :limit
|
|
284
|
+
# (see _resolve_belongs_to_foreign_key_field_spec for the full rationale).
|
|
285
|
+
def _mirror_parent_primary_key(klass, default_spec)
|
|
286
|
+
spec = klass._primary_key_field_spec.foreign_key_field_spec(
|
|
287
|
+
default_spec.model, default_spec.name,
|
|
288
|
+
position: default_spec.position, null: default_spec.null
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Look up the parent's live PK column directly (not via _column, whose
|
|
292
|
+
# @table_exists memoization can pin to a stale value when the parent table
|
|
293
|
+
# is created after the model class is first defined). The rescue covers
|
|
294
|
+
# the table-doesn't-exist-yet case (greenfield migration).
|
|
295
|
+
live_pk_column = klass.columns_hash[klass._declared_primary_key.to_s] rescue nil
|
|
296
|
+
if live_pk_column && live_pk_column.type == spec.type && live_pk_column.limit && live_pk_column.limit != spec.limit
|
|
297
|
+
FieldSpec.new(
|
|
298
|
+
spec.model, spec.name, spec.type,
|
|
299
|
+
position: spec.position,
|
|
300
|
+
**spec.options.merge(limit: live_pk_column.limit)
|
|
301
|
+
)
|
|
302
|
+
else
|
|
303
|
+
spec
|
|
260
304
|
end
|
|
261
305
|
end
|
|
262
306
|
|
|
@@ -273,8 +317,42 @@ module DeclareSchema
|
|
|
273
317
|
end
|
|
274
318
|
end
|
|
275
319
|
|
|
320
|
+
# Returns a FieldSpec for a foreign key pointing to the primary key of this model.
|
|
321
|
+
# Exactly matches the primary key type.
|
|
322
|
+
def _foreign_key_field_spec(model, foreign_key, position:, null:)
|
|
323
|
+
_primary_key_field_spec.foreign_key_field_spec(model, foreign_key, position:, null:)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def _primary_key_field_spec
|
|
327
|
+
declared_primary_key = _declared_primary_key
|
|
328
|
+
field_specs[declared_primary_key] || _primary_key_field_spec_from_table_options(declared_primary_key) or
|
|
329
|
+
raise "Declared primary key #{declared_primary_key.inspect} not found in field_specs or _table_options #{_table_options.inspect} for #{name}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def _primary_key_field_spec_from_table_options(declared_primary_key)
|
|
333
|
+
# _table_options is nil on STI subclasses that never call `declare_schema` themselves:
|
|
334
|
+
# they inherit `field_specs` etc. via `inheriting_cattr_reader`, but `@_table_options`
|
|
335
|
+
# is a plain class-instance variable on each class, so the subclass's reader returns
|
|
336
|
+
# nil. Treat that the same as an empty options hash and fall through to
|
|
337
|
+
# `default_generated_primary_key_type` below.
|
|
338
|
+
type, options = _parse_pk_table_options(_table_options&.[](declared_primary_key.to_sym))
|
|
339
|
+
type ||= DeclareSchema.default_generated_primary_key_type
|
|
340
|
+
FieldSpec.new(self, declared_primary_key, type, **options)
|
|
341
|
+
end
|
|
342
|
+
|
|
276
343
|
private
|
|
277
344
|
|
|
345
|
+
# `declare_schema id: ...` accepts either a Hash (`id: { type: :integer, limit: 4 }`)
|
|
346
|
+
# or a bare type Symbol (`id: :integer`). Returns [type, options_hash], with type == nil
|
|
347
|
+
# when value is neither (caller falls back to a default).
|
|
348
|
+
def _parse_pk_table_options(value)
|
|
349
|
+
case value
|
|
350
|
+
when Hash then [value[:type], value.except(:type)]
|
|
351
|
+
when Symbol then [value, {}]
|
|
352
|
+
else [nil, {}]
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
278
356
|
# if this is a derived class, returns the base class's _declared_primary_key
|
|
279
357
|
# otherwise, returns 'id'
|
|
280
358
|
def _default_declared_primary_key
|
data/lib/declare_schema.rb
CHANGED
|
@@ -162,17 +162,21 @@ module DeclareSchema
|
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
def current_adapter(model_class = ActiveRecord::Base)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
model_class.connection_db_config.adapter
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Default primary key type for generated foreign keys when we have no parent
|
|
169
|
+
# model to mirror (e.g. polymorphic FKs, non-declare_schema parents). Mirrors
|
|
170
|
+
# `config.generators.primary_key_type` so apps that override the Rails default
|
|
171
|
+
# get consistent behavior. Memoized — Rails config is set at boot.
|
|
172
|
+
def default_generated_primary_key_type
|
|
173
|
+
@default_generated_primary_key_type ||=
|
|
174
|
+
Rails.application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint
|
|
170
175
|
end
|
|
171
176
|
end
|
|
172
177
|
end
|
|
173
178
|
|
|
174
179
|
require 'declare_schema/extensions/active_record/fields_declaration'
|
|
175
|
-
require 'declare_schema/field_declaration_dsl'
|
|
176
180
|
require 'declare_schema/model'
|
|
177
181
|
require 'declare_schema/model/field_spec'
|
|
178
182
|
require 'declare_schema/model/index_definition'
|
|
@@ -80,15 +80,13 @@ module Generators
|
|
|
80
80
|
self.class.native_types
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
#
|
|
83
|
+
# return habtm reflections keyed by join table name (so that habtm from each side has just one entry)
|
|
84
84
|
def habtm_tables
|
|
85
|
-
|
|
86
|
-
ActiveRecord::Base.send(:descendants).map do |c|
|
|
85
|
+
ActiveRecord::Base.send(:descendants).each_with_object({}) do |c, result|
|
|
87
86
|
c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
|
|
88
|
-
|
|
87
|
+
result[a.join_table] ||= a
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
|
-
reflections
|
|
92
90
|
end
|
|
93
91
|
|
|
94
92
|
# Returns an array of model classes and an array of table names
|
|
@@ -189,11 +187,7 @@ module Generators
|
|
|
189
187
|
models, db_tables = models_and_tables
|
|
190
188
|
models_by_table_name = {}
|
|
191
189
|
models.each do |m|
|
|
192
|
-
m.try(:field_specs)&.
|
|
193
|
-
if (pre_migration = field_spec.options.delete(:pre_migration))
|
|
194
|
-
pre_migration.call(field_spec)
|
|
195
|
-
end
|
|
196
|
-
end
|
|
190
|
+
m.try(:field_specs)&.transform_values!(&:resolve)
|
|
197
191
|
|
|
198
192
|
if !models_by_table_name.has_key?(m.table_name)
|
|
199
193
|
models_by_table_name[m.table_name] = m
|
|
@@ -204,8 +198,8 @@ module Generators
|
|
|
204
198
|
end
|
|
205
199
|
end
|
|
206
200
|
# generate shims for HABTM models
|
|
207
|
-
habtm_tables.each do |name,
|
|
208
|
-
models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(
|
|
201
|
+
habtm_tables.each do |name, reflection|
|
|
202
|
+
models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(reflection)
|
|
209
203
|
end
|
|
210
204
|
model_table_names = models_by_table_name.keys
|
|
211
205
|
|
|
@@ -376,21 +370,26 @@ module Generators
|
|
|
376
370
|
::DeclareSchema::SchemaChange::ColumnRename.new(new_table_name, old_name, new_name)
|
|
377
371
|
end
|
|
378
372
|
|
|
379
|
-
to_add.sort_by! {
|
|
373
|
+
to_add.sort_by! { model.field_specs[_1]&.position || 0 }
|
|
380
374
|
|
|
381
|
-
adds = to_add.map do |
|
|
375
|
+
adds = to_add.map do |col_name_to_add|
|
|
382
376
|
type, options =
|
|
383
|
-
if (spec = model.field_specs[
|
|
384
|
-
[spec.type, spec.sql_options.
|
|
377
|
+
if (spec = model.field_specs[col_name_to_add])
|
|
378
|
+
[spec.type, spec.sql_options.compact]
|
|
385
379
|
else
|
|
386
|
-
|
|
380
|
+
# No FieldSpec for this column: it's the declared PK appended at line 366
|
|
381
|
+
# because it isn't in the DB yet. Use the configured default PK type so
|
|
382
|
+
# apps that override config.generators.primary_key_type get consistent
|
|
383
|
+
# behavior; the actual PRIMARY KEY designation is added separately by
|
|
384
|
+
# PrimaryKeyChange.
|
|
385
|
+
[::DeclareSchema.default_generated_primary_key_type, {}]
|
|
387
386
|
end
|
|
388
|
-
::DeclareSchema::SchemaChange::ColumnAdd.new(new_table_name,
|
|
387
|
+
::DeclareSchema::SchemaChange::ColumnAdd.new(new_table_name, col_name_to_add, type, **options)
|
|
389
388
|
end
|
|
390
389
|
|
|
391
|
-
removes = to_remove.map do |
|
|
392
|
-
old_type, old_options = add_column_back(model, current_table_name,
|
|
393
|
-
::DeclareSchema::SchemaChange::ColumnRemove.new(new_table_name,
|
|
390
|
+
removes = to_remove.map do |col_name_to_remove|
|
|
391
|
+
old_type, old_options = add_column_back(model, current_table_name, col_name_to_remove)
|
|
392
|
+
::DeclareSchema::SchemaChange::ColumnRemove.new(new_table_name, col_name_to_remove, old_type, **old_options)
|
|
394
393
|
end
|
|
395
394
|
|
|
396
395
|
old_names = to_rename.invert
|
|
@@ -402,13 +401,12 @@ module Generators
|
|
|
402
401
|
spec_attrs = spec.schema_attributes(column)
|
|
403
402
|
column_declaration = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
|
404
403
|
col_attrs = column_declaration.schema_attributes
|
|
405
|
-
normalized_schema_attrs = spec_attrs.merge(fk_field_options(model, col_name_to_change))
|
|
406
404
|
|
|
407
|
-
if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(
|
|
408
|
-
type =
|
|
405
|
+
if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(spec_attrs, col_attrs)
|
|
406
|
+
type = spec_attrs.delete(:type) or raise "no :type found in #{spec_attrs.inspect}"
|
|
409
407
|
old_type, old_options = change_column_back(model, current_table_name, orig_col_name)
|
|
410
408
|
changes << ::DeclareSchema::SchemaChange::ColumnChange.new(new_table_name, col_name_to_change,
|
|
411
|
-
new_type: type, new_options:
|
|
409
|
+
new_type: type, new_options: spec_attrs,
|
|
412
410
|
old_type: old_type, old_options: old_options)
|
|
413
411
|
end
|
|
414
412
|
end
|
|
@@ -546,22 +544,6 @@ module Generators
|
|
|
546
544
|
end
|
|
547
545
|
end
|
|
548
546
|
|
|
549
|
-
def fk_field_options(model, field_name)
|
|
550
|
-
if (foreign_key = model.constraint_definitions.find { |fk| field_name == fk.foreign_key_column })
|
|
551
|
-
parent_columns = connection.columns(foreign_key.parent_table_name) rescue []
|
|
552
|
-
pk_limit =
|
|
553
|
-
if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
|
|
554
|
-
pk_column.limit
|
|
555
|
-
else
|
|
556
|
-
8
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
{ limit: pk_limit }
|
|
560
|
-
else
|
|
561
|
-
{}
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
|
|
565
547
|
def change_table_options(model, current_table_name)
|
|
566
548
|
old_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.for_model(model, current_table_name)
|
|
567
549
|
new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, **table_options_for_model(model))
|
|
@@ -25,6 +25,38 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
describe '#resolve' do
|
|
29
|
+
let(:default_spec) { described_class.new(model, :advertiser_id, :integer, limit: 8, null: false, position: 1) }
|
|
30
|
+
let(:mirrored_spec) { described_class.new(model, :advertiser_id, :string, limit: 36, null: false, position: 1) }
|
|
31
|
+
|
|
32
|
+
context 'when no resolver was supplied' do
|
|
33
|
+
it 'returns self' do
|
|
34
|
+
expect(default_spec.resolve).to equal(default_spec)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context 'when a resolver was supplied' do
|
|
39
|
+
it 'invokes the resolver with self and returns its result' do
|
|
40
|
+
captured = nil
|
|
41
|
+
spec_under_test = described_class.new(model, :advertiser_id, :integer, limit: 8, null: false, position: 1,
|
|
42
|
+
resolver: ->(spec) { captured = spec; mirrored_spec })
|
|
43
|
+
|
|
44
|
+
expect(spec_under_test.resolve).to equal(mirrored_spec)
|
|
45
|
+
expect(captured).to equal(spec_under_test)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'memoizes the resolver result' do
|
|
49
|
+
call_count = 0
|
|
50
|
+
spec_under_test = described_class.new(model, :advertiser_id, :integer, limit: 8, null: false, position: 1,
|
|
51
|
+
resolver: ->(_spec) { call_count += 1; mirrored_spec })
|
|
52
|
+
|
|
53
|
+
3.times { spec_under_test.resolve }
|
|
54
|
+
|
|
55
|
+
expect(call_count).to eq(1)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
28
60
|
describe '#schema_attributes' do
|
|
29
61
|
describe 'integer 4' do
|
|
30
62
|
subject { described_class.new(model, :price, :integer, limit: 4, null: false, position: 0) }
|
|
@@ -988,6 +988,140 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
|
988
988
|
nuke_model_class(Creative)
|
|
989
989
|
end
|
|
990
990
|
|
|
991
|
+
context 'foreign keys mirror non-default primary key types' do
|
|
992
|
+
after do
|
|
993
|
+
nuke_model_class(SpecialOrder) if defined?(SpecialOrder)
|
|
994
|
+
nuke_model_class(Order) if defined?(Order)
|
|
995
|
+
nuke_model_class(LineItem) if defined?(LineItem)
|
|
996
|
+
nuke_model_class(Tag) if defined?(Tag)
|
|
997
|
+
nuke_model_class(Product) if defined?(Product)
|
|
998
|
+
nuke_model_class(ProductsTagsClass) if defined?(ProductsTagsClass)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
it 'belongs_to defers parent class lookup to migration time (no dependency cycle)' do
|
|
1002
|
+
# LineItem.belongs_to :order is allowed to mention :order before Order is loaded.
|
|
1003
|
+
# If we eagerly resolved reflection.klass at belongs_to time this would raise
|
|
1004
|
+
# NameError: uninitialized constant LineItem::Order.
|
|
1005
|
+
expect do
|
|
1006
|
+
class LineItem < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1007
|
+
declare_schema { integer :quantity, null: false }
|
|
1008
|
+
belongs_to :order
|
|
1009
|
+
end
|
|
1010
|
+
end.to_not raise_error
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
it 'belongs_to mirrors the parent primary key type and limit (declared via _table_options)' do
|
|
1014
|
+
# Order has a non-default :string primary key; LineItem.belongs_to :order should
|
|
1015
|
+
# produce a :string foreign key with the same limit -- without forcing Order to
|
|
1016
|
+
# load when LineItem is defined.
|
|
1017
|
+
class LineItem < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1018
|
+
declare_schema do
|
|
1019
|
+
integer :quantity, null: false
|
|
1020
|
+
end
|
|
1021
|
+
belongs_to :order
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
class Order < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1025
|
+
self.primary_key = "id"
|
|
1026
|
+
declare_schema id: { type: :string, limit: 36 } do
|
|
1027
|
+
string :customer_email, limit: 250, null: false
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run
|
|
1032
|
+
|
|
1033
|
+
expect(up).to match(/t\.string\s+:order_id, limit: 36, null: false/)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
context 'when belongs_to points at an STI subclass' do
|
|
1037
|
+
# An STI subclass inherits field_specs (via inheriting_cattr_reader) and the
|
|
1038
|
+
# `_primary_key_field_spec` class method (mixed into its parent), but it never
|
|
1039
|
+
# calls `declare_schema` on itself, so its own `@_table_options` is nil. When a
|
|
1040
|
+
# belongs_to points at the STI subclass, the migrator's resolver calls
|
|
1041
|
+
# `klass._primary_key_field_spec`, which previously crashed with
|
|
1042
|
+
# `NoMethodError: undefined method '[]' for nil` because
|
|
1043
|
+
# `_primary_key_field_spec_from_table_options` indexed `_table_options` without
|
|
1044
|
+
# a nil guard.
|
|
1045
|
+
before do
|
|
1046
|
+
class Order < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1047
|
+
declare_schema do
|
|
1048
|
+
string :customer_email, limit: 250, null: false
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
class SpecialOrder < Order # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
class LineItem < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1056
|
+
declare_schema do
|
|
1057
|
+
integer :quantity, null: false
|
|
1058
|
+
end
|
|
1059
|
+
belongs_to :special_order
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
let(:up) { Generators::DeclareSchema::Migration::Migrator.run.first }
|
|
1064
|
+
|
|
1065
|
+
it 'mirrors the base class primary key without crashing' do
|
|
1066
|
+
expect(up).to match(/t\.integer\s+:special_order_id, limit: 8, null: false/)
|
|
1067
|
+
end
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
context 'when a HABTM join table has a parent PK type that drifts from the live column' do
|
|
1071
|
+
before do
|
|
1072
|
+
conn = ActiveRecord::Base.connection
|
|
1073
|
+
conn.create_table(:products) { |t| t.string :name, limit: 100, null: false }
|
|
1074
|
+
conn.create_table(:tags) { |t| t.string :label, limit: 50, null: false }
|
|
1075
|
+
conn.create_table(:products_tags, primary_key: [:product_id, :tag_id]) do |t|
|
|
1076
|
+
t.bigint :product_id, null: false
|
|
1077
|
+
t.bigint :tag_id, null: false
|
|
1078
|
+
end
|
|
1079
|
+
conn.schema_cache.clear!
|
|
1080
|
+
|
|
1081
|
+
class Product < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1082
|
+
declare_schema do
|
|
1083
|
+
string :name, limit: 100, null: false
|
|
1084
|
+
end
|
|
1085
|
+
has_and_belongs_to_many :tags
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
class Tag < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1089
|
+
declare_schema id: { type: :string, limit: 36 } do
|
|
1090
|
+
string :label, limit: 50, null: false
|
|
1091
|
+
end
|
|
1092
|
+
has_and_belongs_to_many :products
|
|
1093
|
+
end
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
let(:up) { Generators::DeclareSchema::Migration::Migrator.run.first }
|
|
1097
|
+
|
|
1098
|
+
it 'generates change_column on the join table without crashing' do
|
|
1099
|
+
expect(up).to match(/change_column\s+:products_tags,\s+:tag_id,\s+:string/)
|
|
1100
|
+
end
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
it 'HABTM mirrors both parents primary key types (different non-default types)' do
|
|
1104
|
+
class Product < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1105
|
+
declare_schema id: { type: :string, limit: 36 } do
|
|
1106
|
+
string :name, limit: 100, null: false
|
|
1107
|
+
end
|
|
1108
|
+
has_and_belongs_to_many :tags
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
class Tag < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
1112
|
+
declare_schema id: { type: :integer, limit: 4 } do
|
|
1113
|
+
string :label, limit: 50, null: false
|
|
1114
|
+
end
|
|
1115
|
+
has_and_belongs_to_many :products
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
up, _ = Generators::DeclareSchema::Migration::Migrator.run
|
|
1119
|
+
|
|
1120
|
+
expect(up).to match(/t\.string\s+:product_id, limit: 36, null: false/)
|
|
1121
|
+
expect(up).to match(/t\.integer\s+:tag_id, limit: 4, null: false/)
|
|
1122
|
+
end
|
|
1123
|
+
end
|
|
1124
|
+
|
|
991
1125
|
context 'models with the same parent foreign key relation' do
|
|
992
1126
|
include_context 'skip if' do
|
|
993
1127
|
let(:adapter) { 'sqlite3' }
|
|
@@ -41,7 +41,12 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
describe 'instance methods' do
|
|
44
|
-
|
|
44
|
+
let(:parent_models) { [User, Customer] }
|
|
45
|
+
let(:parents) { foreign_keys.zip(parent_models) }
|
|
46
|
+
|
|
47
|
+
subject do
|
|
48
|
+
described_class.new(join_table, parents, connection: connection)
|
|
49
|
+
end
|
|
45
50
|
|
|
46
51
|
describe '#initialize' do
|
|
47
52
|
it 'stores initialization attributes' do
|
|
@@ -88,8 +93,8 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
|
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
describe '#primary_key' do
|
|
91
|
-
it 'returns
|
|
92
|
-
expect(subject.primary_key).to eq(
|
|
96
|
+
it 'returns the composite of the two foreign keys (alphabetically sorted)' do
|
|
97
|
+
expect(subject.primary_key).to eq(["customer_id", "user_id"])
|
|
93
98
|
end
|
|
94
99
|
end
|
|
95
100
|
|
|
@@ -116,14 +121,15 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
|
|
|
116
121
|
let(:foreign_keys_and_table_names) { [["advertiser_id", "advertisers"], ["campaign_id", "campaigns"]] }
|
|
117
122
|
let(:foreign_keys) { foreign_keys_and_table_names.map(&:first) }
|
|
118
123
|
let(:parent_table_names) { foreign_keys_and_table_names.map(&:last) }
|
|
124
|
+
let(:parent_models) { [Table1, Table2] }
|
|
119
125
|
|
|
120
126
|
before do
|
|
121
|
-
class Table1 < ActiveRecord::Base
|
|
122
|
-
self.table_name = '
|
|
127
|
+
class Table1 < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
128
|
+
self.table_name = 'advertisers'
|
|
123
129
|
end
|
|
124
130
|
|
|
125
|
-
class Table2 < ActiveRecord::Base
|
|
126
|
-
self.table_name = '
|
|
131
|
+
class Table2 < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
132
|
+
self.table_name = 'campaigns'
|
|
127
133
|
end
|
|
128
134
|
end
|
|
129
135
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'generators/declare_schema/support/thor_shell'
|
|
4
|
+
|
|
5
|
+
# Verifies that when the migrator has to add a declared primary key column
|
|
6
|
+
# that has no corresponding FieldSpec (because it was appended to `to_add`
|
|
7
|
+
# at lib/generators/declare_schema/migration/migrator.rb:366), the column
|
|
8
|
+
# type is derived from `DeclareSchema.default_generated_primary_key_type`
|
|
9
|
+
# rather than a hardcoded `:integer`. This matters for apps that override
|
|
10
|
+
# `config.generators.primary_key_type`.
|
|
11
|
+
RSpec.describe 'DeclareSchema.default_generated_primary_key_type integration with Migrator' do
|
|
12
|
+
include_context 'prepare test app'
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
if current_adapter == 'mysql2'
|
|
16
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE foos (id int PRIMARY KEY, name varchar(250))")
|
|
17
|
+
else
|
|
18
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE foos (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(250))")
|
|
19
|
+
end
|
|
20
|
+
ActiveRecord::Base.connection.schema_cache.clear!
|
|
21
|
+
|
|
22
|
+
allow_any_instance_of(DeclareSchema::Support::ThorShell)
|
|
23
|
+
.to receive(:ask).with(/one of the rename choices or press enter to keep/) { 'drop id' }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'derives the new PK column type from DeclareSchema.default_generated_primary_key_type rather than a hardcoded :integer' do
|
|
27
|
+
allow(::DeclareSchema).to receive(:default_generated_primary_key_type).and_return(:string)
|
|
28
|
+
|
|
29
|
+
class Foo < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
30
|
+
declare_schema do
|
|
31
|
+
string :name, limit: 250
|
|
32
|
+
end
|
|
33
|
+
self.primary_key = "foo_id"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
|
37
|
+
|
|
38
|
+
expect(up).to include("add_column :foos, :foo_id, :string")
|
|
39
|
+
expect(up).not_to include("add_column :foos, :foo_id, :integer")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'falls through to the configured Rails default (:bigint) when the helper is not stubbed' do
|
|
43
|
+
class Foo < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinitionInBlock
|
|
44
|
+
declare_schema do
|
|
45
|
+
string :name, limit: 250
|
|
46
|
+
end
|
|
47
|
+
self.primary_key = "foo_id"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
up, _down = Generators::DeclareSchema::Migration::Migrator.run
|
|
51
|
+
|
|
52
|
+
expect(up).to include("add_column :foos, :foo_id, #{::DeclareSchema.default_generated_primary_key_type.inspect}")
|
|
53
|
+
end
|
|
54
|
+
end
|