declare_schema 1.4.0.colin.7 → 1.4.0.colin.9

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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "1.4.0.colin.7"
4
+ VERSION = "1.4.0.colin.9"
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_support'
4
4
  require 'active_support/all'
5
5
  require_relative 'declare_schema/version'
6
+ require 'rubygems/version'
6
7
 
7
8
  ActiveSupport::Dependencies.autoload_paths |= [__dir__]
8
9
 
@@ -21,6 +22,8 @@ module DeclareSchema
21
22
  text: String
22
23
  }.freeze
23
24
 
25
+ SEMVER_8 = Gem::Version.new('8.0.0').freeze
26
+
24
27
  @default_charset = "utf8mb4"
25
28
  @default_collation = "utf8mb4_bin"
26
29
  @default_text_limit = 0xffff_ffff
@@ -32,6 +35,7 @@ module DeclareSchema
32
35
  @max_index_and_constraint_name_length = 64 # limit for MySQL
33
36
 
34
37
  class << self
38
+ attr_writer :mysql_version
35
39
  attr_reader :default_charset, :default_collation, :default_text_limit, :default_string_limit, :default_null,
36
40
  :default_generate_foreign_keys, :default_generate_indexing, :db_migrate_command,
37
41
  :max_index_and_constraint_name_length
@@ -47,14 +51,42 @@ module DeclareSchema
47
51
  end
48
52
  end
49
53
 
54
+ def mysql_version
55
+ if defined?(@mysql_version)
56
+ @mysql_version
57
+ else
58
+ @mysql_version =
59
+ if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
60
+ version_string = ActiveRecord::Base.connection.select_value('SELECT VERSION()')
61
+ Gem::Version.new(version_string)
62
+ end
63
+ end
64
+ end
65
+
66
+ def normalize_charset(charset)
67
+ if mysql_version && mysql_version >= SEMVER_8 && charset == 'utf8'
68
+ 'utf8mb3'
69
+ else
70
+ charset
71
+ end
72
+ end
73
+
74
+ def normalize_collation(collation)
75
+ if mysql_version && mysql_version >= SEMVER_8
76
+ collation.sub(/\Autf8_/, 'utf8mb3_')
77
+ else
78
+ collation
79
+ end
80
+ end
81
+
50
82
  def default_charset=(charset)
51
83
  charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
52
- @default_charset = charset
84
+ @default_charset = normalize_charset(charset)
53
85
  end
54
86
 
55
87
  def default_collation=(collation)
56
88
  collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
57
- @default_collation = collation
89
+ @default_collation = normalize_collation(collation)
58
90
  end
59
91
 
60
92
  def default_text_limit=(text_limit)
@@ -3,6 +3,7 @@
3
3
  require 'active_record'
4
4
  require 'active_record/connection_adapters/abstract_adapter'
5
5
  require 'declare_schema/schema_change/all'
6
+ require_relative '../../../declare_schema/model/habtm_model_shim'
6
7
 
7
8
  module Generators
8
9
  module DeclareSchema
@@ -203,8 +204,8 @@ module Generators
203
204
  end
204
205
  end
205
206
  # generate shims for HABTM models
206
- habtm_tables.each do |name, refls|
207
- models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(refls.first)
207
+ habtm_tables.each do |name, reflections|
208
+ models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(reflections.first)
208
209
  end
209
210
  model_table_names = models_by_table_name.keys
210
211
 
@@ -221,8 +222,8 @@ module Generators
221
222
  ::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
222
223
  end
223
224
 
224
- creates = to_create.map do |t|
225
- model = models_by_table_name[t]
225
+ creates = to_create.map do |table_name|
226
+ model = models_by_table_name[table_name]
226
227
  disable_auto_increment = model.try(:disable_auto_increment)
227
228
 
228
229
  primary_key_definition =
@@ -239,10 +240,13 @@ module Generators
239
240
  table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, **table_options_for_model(model))
240
241
  table_options = create_table_options(model, disable_auto_increment)
241
242
 
242
- table_add = ::DeclareSchema::SchemaChange::TableAdd.new(t,
243
- primary_key_definition + field_definitions,
244
- table_options,
245
- sql_options: table_options_definition.settings)
243
+ table_add = ::DeclareSchema::SchemaChange::TableAdd.new(
244
+ table_name,
245
+ primary_key_definition + field_definitions,
246
+ table_options,
247
+ sql_options: table_options_definition.settings
248
+ )
249
+
246
250
  [
247
251
  table_add,
248
252
  *Array((create_indexes(model) if ::DeclareSchema.default_generate_indexing)),
@@ -255,9 +259,9 @@ module Generators
255
259
  fk_changes = []
256
260
  table_options_changes = []
257
261
 
258
- to_change.each do |t|
259
- model = models_by_table_name[t]
260
- table = to_rename.key(t) || model.table_name
262
+ to_change.each do |table_name|
263
+ model = models_by_table_name[table_name]
264
+ table = to_rename.key(table_name) || model.table_name
261
265
  if table.in?(db_tables)
262
266
  change, index_change, fk_change, table_options_change = change_table(model, table)
263
267
  changes << change
@@ -311,6 +315,8 @@ module Generators
311
315
  { id: false }
312
316
  elsif primary_key == "id"
313
317
  { id: :bigint }
318
+ elsif primary_key.is_a?(Array)
319
+ { primary_key: primary_key.map(&:to_sym) }
314
320
  else
315
321
  { primary_key: primary_key.to_sym }
316
322
  end.merge(model._table_options)
@@ -335,9 +341,9 @@ module Generators
335
341
  end
336
342
 
337
343
  def create_constraints(model)
338
- model.constraint_specs.map do |fk|
344
+ model.constraint_definitions.map do |fk|
339
345
  ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
340
- column_name: fk.foreign_key_name, name: fk.constraint_name)
346
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
341
347
  end
342
348
  end
343
349
 
@@ -425,7 +431,7 @@ module Generators
425
431
  ::DeclareSchema.default_generate_indexing or return []
426
432
 
427
433
  new_table_name = model.table_name
428
- existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
434
+ existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_table(old_table_name || new_table_name, model.ignore_indexes, model.connection)
429
435
  model_indexes_with_equivalents = model.index_definitions_with_primary_key.to_a
430
436
  model_indexes = model_indexes_with_equivalents.map do |i|
431
437
  if i.explicit_name.nil?
@@ -485,8 +491,12 @@ module Generators
485
491
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
486
492
  ::DeclareSchema.default_generate_foreign_keys or return []
487
493
 
488
- existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
489
- model_fks = model.constraint_specs.to_a
494
+ if model.is_a?(::DeclareSchema::Model::HabtmModelShim)
495
+ force_dependent_delete = :delete
496
+ end
497
+
498
+ existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_table(old_table_name || model.table_name, model.connection, dependent: force_dependent_delete)
499
+ model_fks = model.constraint_definitions.to_a
490
500
 
491
501
  fks_to_drop = existing_fks - model_fks
492
502
  fks_to_add = model_fks - existing_fks
@@ -495,13 +505,13 @@ module Generators
495
505
 
496
506
  drop_fks = (fks_to_drop - renamed_fks_to_drop).map do |fk|
497
507
  ::DeclareSchema::SchemaChange::ForeignKeyRemove.new(fk.child_table_name, fk.parent_table_name,
498
- column_name: fk.foreign_key_name, name: fk.constraint_name)
508
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
499
509
  end
500
510
 
501
511
  add_fks = (fks_to_add - renamed_fks_to_add).map do |fk|
502
512
  # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
503
513
  ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
504
- column_name: fk.foreign_key_name, name: fk.constraint_name)
514
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
505
515
  end
506
516
 
507
517
  [drop_fks + add_fks]
@@ -523,9 +533,8 @@ module Generators
523
533
  end
524
534
 
525
535
  def fk_field_options(model, field_name)
526
- foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }
527
- if foreign_key && (parent_table = foreign_key.parent_table_name)
528
- parent_columns = connection.columns(parent_table) rescue []
536
+ if (foreign_key = model.constraint_definitions.find { |fk| field_name == fk.foreign_key_column })
537
+ parent_columns = connection.columns(foreign_key.parent_table_name) rescue []
529
538
  pk_limit =
530
539
  if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
531
540
  pk_column.limit
@@ -585,6 +594,8 @@ module Generators
585
594
  case charset
586
595
  when "utf8"
587
596
  "utf8_general_ci"
597
+ when "utf8mb3"
598
+ "utf8mb3_general_ci"
588
599
  when "utf8mb4"
589
600
  "utf8mb4_general_ci"
590
601
  end
@@ -5,6 +5,11 @@ begin
5
5
  rescue LoadError
6
6
  end
7
7
 
8
+ begin
9
+ require 'sqlite3'
10
+ rescue LoadError
11
+ end
12
+
8
13
  RSpec.describe DeclareSchema::Model::FieldSpec do
9
14
  let(:model) { double('model', _table_options: {}, _declared_primary_key: 'id') }
10
15
  let(:col_spec) { double('col_spec', type: :string) }
@@ -58,8 +63,23 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
58
63
  end
59
64
  end
60
65
 
61
- it 'raises error when default_string_limit option is nil when not explicitly set in field spec' do
62
- if defined?(Mysql2)
66
+ if defined?(Mysql2)
67
+ context 'when running on MySQL 8.0' do
68
+ around do |spec|
69
+ DeclareSchema.mysql_version = Gem::Version.new('8.0.21')
70
+ spec.run
71
+ ensure
72
+ DeclareSchema.remove_instance_variable('@mysql_version') rescue nil
73
+ end
74
+
75
+ it 'normalizes charset and collation' do
76
+ subject = described_class.new(model, :title, :string, limit: 100, null: true, charset: 'utf8', collation: 'utf8_general', position: 0)
77
+
78
+ expect(subject.schema_attributes(col_spec)).to eq(type: :string, limit: 100, null: true, charset: 'utf8mb3', collation: 'utf8mb3_general')
79
+ end
80
+ end
81
+
82
+ it 'raises error when default_string_limit option is nil when not explicitly set in field spec' do
63
83
  expect(::DeclareSchema).to receive(:default_string_limit) { nil }
64
84
  expect do
65
85
  described_class.new(model, :title, :string, null: true, charset: 'utf8mb4', position: 0)
@@ -391,6 +391,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
391
391
 
392
392
  Advert.field_specs.delete(:c_id)
393
393
  Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
394
+ Advert.constraint_definitions.delete_if { |spec| spec.foreign_key_column == "c_id" }
394
395
 
395
396
  # You can avoid generating the index by specifying `index: false`
396
397
 
@@ -403,17 +404,17 @@ RSpec.describe 'DeclareSchema Migration Generator' do
403
404
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
404
405
  migrate_up(<<~EOS.strip)
405
406
  add_column :adverts, :category_id, :integer, limit: 8, null: false
406
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
407
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
407
+ #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id" if defined?(Mysql2)}
408
408
  EOS
409
409
  )
410
410
 
411
411
  Advert.field_specs.delete(:category_id)
412
412
  Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
413
+ Advert.constraint_definitions.delete_if { |spec| spec.foreign_key_column == "category_id" }
413
414
 
414
415
  # You can specify the index name with index: 'name' [deprecated]
415
416
 
416
- expect(Kernel).to receive(:warn).with(/belongs_to index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
417
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/belongs_to :category, index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
417
418
 
418
419
  class Category < ActiveRecord::Base; end
419
420
  class Advert < ActiveRecord::Base
@@ -425,13 +426,13 @@ RSpec.describe 'DeclareSchema Migration Generator' do
425
426
  migrate_up(<<~EOS.strip)
426
427
  add_column :adverts, :category_id, :integer, limit: 8, null: false
427
428
  add_index :adverts, [:category_id], name: :my_index
428
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
429
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
429
+ #{"add_foreign_key :adverts, :categories, column: :category_id, name: :my_index" if defined?(Mysql2)}
430
430
  EOS
431
431
  )
432
432
 
433
433
  Advert.field_specs.delete(:category_id)
434
434
  Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
435
+ Advert.constraint_definitions.delete_if { |spec| spec.foreign_key_column == "category_id" }
435
436
 
436
437
  # You can specify the index name with index: { name: }'name', unique: true|false }
437
438
 
@@ -443,15 +444,15 @@ RSpec.describe 'DeclareSchema Migration Generator' do
443
444
 
444
445
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
445
446
  migrate_up(<<~EOS.strip)
446
- add_column :adverts, :category_id, :integer, limit: 8, null: false
447
- add_index :adverts, [:category_id], name: :my_index
448
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
449
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
450
- EOS
447
+ add_column :adverts, :category_id, :integer, limit: 8, null: false
448
+ add_index :adverts, [:category_id], name: :my_index
449
+ #{"add_foreign_key :adverts, :categories, column: :category_id, name: :my_index" if defined?(Mysql2)}
450
+ EOS
451
451
  )
452
452
 
453
453
  Advert.field_specs.delete(:category_id)
454
454
  Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
455
+ Advert.constraint_definitions.delete_if { |spec| spec.foreign_key_column == "category_id" }
455
456
 
456
457
  ### Timestamps and Optimimistic Locking
457
458
 
@@ -470,12 +471,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
470
471
  add_column :adverts, :created_at, :datetime, null: true
471
472
  add_column :adverts, :updated_at, :datetime, null: true
472
473
  add_column :adverts, :lock_version, :integer#{lock_version_limit}, null: false, default: 1
473
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
474
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
475
474
  EOS
476
475
  .and migrate_down(<<~EOS.strip)
477
- #{"remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
478
- "remove_foreign_key :adverts, name: :index_adverts_on_category_id" if defined?(Mysql2)}
479
476
  remove_column :adverts, :lock_version
480
477
  remove_column :adverts, :updated_at
481
478
  remove_column :adverts, :created_at
@@ -490,8 +487,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
490
487
 
491
488
  # You can add an index to a field definition
492
489
 
493
- expect(Kernel).to receive(:warn).with(/belongs_to index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
494
- expect(Kernel).to receive(:warn).with(/belongs_to unique: true\|false is deprecated; use index: \{ unique: true\|false \} instead/i)
490
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/belongs_to :category, index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
491
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/belongs_to :category, unique: true\|false is deprecated; use index: \{ unique: true\|false \} instead/i)
495
492
 
496
493
  class Advert < ActiveRecord::Base
497
494
  declare_schema do
@@ -506,15 +503,15 @@ RSpec.describe 'DeclareSchema Migration Generator' do
506
503
  add_column :adverts, :category_id, :integer, limit: 8, null: false
507
504
  add_index :adverts, [:title], name: :index_adverts_on_title
508
505
  add_index :adverts, [:category_id], name: :my_index
509
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
510
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
506
+ #{"add_foreign_key :adverts, :categories, column: :category_id, name: :my_index" if defined?(Mysql2)}
511
507
  EOS
512
508
  )
513
509
 
514
510
  Advert.field_specs.delete(:category_id)
515
511
  Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] || spec.fields == ["category_id"] }
512
+ Advert.constraint_definitions.delete_if { |spec| spec.foreign_key_column == "category_id" }
516
513
 
517
- # You can ask for a unique index
514
+ # You can ask for a unique index (deprecated syntax; use index: { unique: true } instead).
518
515
 
519
516
  class Advert < ActiveRecord::Base
520
517
  declare_schema do
@@ -526,8 +523,6 @@ RSpec.describe 'DeclareSchema Migration Generator' do
526
523
  migrate_up(<<~EOS.strip)
527
524
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
528
525
  add_index :adverts, [:title], name: :index_adverts_on_title, unique: true
529
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
530
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
531
526
  EOS
532
527
  )
533
528
 
@@ -545,8 +540,6 @@ RSpec.describe 'DeclareSchema Migration Generator' do
545
540
  migrate_up(<<~EOS.strip)
546
541
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
547
542
  add_index :adverts, [:title], name: :my_index
548
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
549
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
550
543
  EOS
551
544
  )
552
545
 
@@ -555,6 +548,9 @@ RSpec.describe 'DeclareSchema Migration Generator' do
555
548
  # You can ask for an index outside of the fields block
556
549
 
557
550
  class Advert < ActiveRecord::Base
551
+ declare_schema do
552
+ string :title, limit: 250, null: true
553
+ end
558
554
  index :title
559
555
  end
560
556
 
@@ -562,8 +558,6 @@ RSpec.describe 'DeclareSchema Migration Generator' do
562
558
  migrate_up(<<~EOS.strip)
563
559
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
564
560
  add_index :adverts, [:title], name: :index_adverts_on_title
565
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
566
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
567
561
  EOS
568
562
  )
569
563
 
@@ -578,9 +572,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
578
572
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
579
573
  migrate_up(<<~EOS.strip)
580
574
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
581
- add_index :adverts, [:title], name: :my_index, length: 10
582
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
583
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
575
+ add_index :adverts, [:title], name: :my_index, length: { title: 10 }
584
576
  EOS
585
577
  )
586
578
 
@@ -596,8 +588,6 @@ RSpec.describe 'DeclareSchema Migration Generator' do
596
588
  migrate_up(<<~EOS.strip)
597
589
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
598
590
  add_index :adverts, [:title, :category_id], name: :index_adverts_on_title_and_category_id
599
- #{"add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
600
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id" if defined?(Mysql2)}
601
591
  EOS
602
592
  )
603
593
 
@@ -627,16 +617,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
627
617
  rename_table :adverts, :ads
628
618
  add_column :ads, :title, :string, limit: 250, null: true#{charset_and_collation}
629
619
  add_column :ads, :body, :text#{', limit: 4294967295' if defined?(Mysql2)}, null: true#{charset_and_collation}
630
- #{if defined?(Mysql2)
631
- "add_foreign_key :adverts, :categories, column: :category_id, name: :index_adverts_on_category_id\n" +
632
- "add_foreign_key :adverts, :categories, column: :c_id, name: :index_adverts_on_c_id"
633
- end}
634
- EOS
620
+ EOS
635
621
  .and migrate_down(<<~EOS.strip)
636
- #{if defined?(Mysql2)
637
- "remove_foreign_key :adverts, name: :index_adverts_on_c_id\n" +
638
- "remove_foreign_key :adverts, name: :index_adverts_on_category_id"
639
- end}
640
622
  remove_column :ads, :body
641
623
  remove_column :ads, :title
642
624
  rename_table :ads, :adverts
@@ -838,6 +820,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
838
820
  expect(User.field_specs.keys).to eq(['company'])
839
821
  expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
840
822
 
823
+ nuke_model_class(User)
824
+
841
825
  ## validates
842
826
 
843
827
  # DeclareSchema can accept a validates hash in the field options.
@@ -858,6 +842,44 @@ RSpec.describe 'DeclareSchema Migration Generator' do
858
842
  up, _down = Generators::DeclareSchema::Migration::Migrator.run
859
843
  ActiveRecord::Migration.class_eval(up)
860
844
  expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
845
+
846
+ # DeclareSchema supports has_and_belongs_to_many relationships and generates the intersection ("join") table
847
+ # with appropriate primary key, indexes, and foreign keys.
848
+
849
+ class Advertiser < ActiveRecord::Base
850
+ declare_schema do
851
+ string :name, limit: 250
852
+ end
853
+ has_and_belongs_to_many :creatives
854
+ end
855
+ class Creative < ActiveRecord::Base
856
+ declare_schema do
857
+ string :url, limit: 500
858
+ end
859
+ has_and_belongs_to_many :advertisers
860
+ end
861
+
862
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to(
863
+ migrate_up(<<~EOS.strip)
864
+ create_table :advertisers, id: :bigint#{create_table_charset_and_collation} do |t|
865
+ t.string :name, limit: 250, null: false#{charset_and_collation}
866
+ end
867
+ create_table :advertisers_creatives, primary_key: [:advertiser_id, :creative_id]#{create_table_charset_and_collation} do |t|
868
+ t.integer :advertiser_id, limit: 8, null: false
869
+ t.integer :creative_id, limit: 8, null: false
870
+ end
871
+ create_table :creatives, id: :bigint#{create_table_charset_and_collation} do |t|
872
+ t.string :url, limit: 500, null: false#{charset_and_collation}
873
+ end
874
+ add_index :advertisers_creatives, [:creative_id], name: :index_advertisers_creatives_on_creative_id
875
+ add_foreign_key :advertisers_creatives, :advertisers, column: :advertiser_id, name: :advertisers_creatives_FK1
876
+ add_foreign_key :advertisers_creatives, :creatives, column: :creative_id, name: :advertisers_creatives_FK2
877
+ EOS
878
+ )
879
+
880
+ nuke_model_class(Ad)
881
+ nuke_model_class(Advertiser)
882
+ nuke_model_class(Creative)
861
883
  end
862
884
 
863
885
  context 'models with the same parent foreign key relation' do
@@ -881,7 +903,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
881
903
  end
882
904
  end
883
905
 
884
- it 'will genereate unique constraint names' do
906
+ it 'will generate unique constraint names' do
885
907
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
886
908
  migrate_up(<<~EOS.strip)
887
909
  create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
@@ -1178,7 +1200,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1178
1200
  end
1179
1201
 
1180
1202
  it 'deprecates limit:' do
1181
- expect(ActiveSupport::Deprecation).to receive(:warn).with("belongs_to limit: is deprecated since it is now inferred")
1203
+ expect(ActiveSupport::Deprecation).to receive(:warn).with("belongs_to :ad_category, limit: is deprecated since it is now inferred")
1182
1204
  eval <<~EOS
1183
1205
  class UsingLimit < ActiveRecord::Base
1184
1206
  declare_schema { }
@@ -21,9 +21,9 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
21
21
  describe 'instance methods' do
22
22
  let(:connection) { instance_double(ActiveRecord::Base.connection.class) }
23
23
  let(:model) { instance_double('Model', table_name: 'models', connection: connection) }
24
- let(:foreign_key) { :network_id }
25
- let(:options) { {} }
26
- subject { described_class.new(model, foreign_key, **options)}
24
+ let(:foreign_key_column) { :network_id }
25
+ let(:options) { { child_table_name: 'advertisers' } }
26
+ subject { described_class.new(foreign_key_column, **options)}
27
27
 
28
28
  before do
29
29
  allow(model.connection).to receive(:index_name).with(any_args) { 'index_on_network_id' }
@@ -31,63 +31,64 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
31
31
 
32
32
  describe '#initialize' do
33
33
  it 'normalizes symbols to strings' do
34
- expect(subject.foreign_key).to eq('network_id')
34
+ expect(subject.foreign_key_column).to eq('network_id')
35
35
  expect(subject.parent_table_name).to eq('networks')
36
36
  end
37
37
 
38
38
  context 'when most options passed' do
39
- let(:options) { { parent_table: :networks, foreign_key: :the_network_id } }
39
+ let(:options) { { child_table_name: 'advertisers', parent_class_name: 'Network' } }
40
40
 
41
41
  it 'normalizes symbols to strings' do
42
- expect(subject.foreign_key).to eq('network_id')
43
- expect(subject.foreign_key_name).to eq('the_network_id')
42
+ expect(subject.foreign_key_column).to eq('network_id')
44
43
  expect(subject.parent_table_name).to eq('networks')
45
- expect(subject.foreign_key).to eq('network_id')
46
- expect(subject.constraint_name).to eq('index_on_network_id')
47
- expect(subject.on_delete_cascade).to be_falsey
44
+ expect(subject.foreign_key_column).to eq('network_id')
45
+ expect(subject.constraint_name).to eq('index_advertisers_on_network_id')
46
+ expect(subject.dependent).to be_nil
48
47
  end
49
48
  end
50
49
 
51
50
  context 'when all options passed' do
52
- let(:options) { { parent_table: :networks, foreign_key: :the_network_id, constraint_name: :constraint_1, dependent: :delete } }
51
+ let(:options) { { child_table_name: 'advertisers', parent_class_name: 'Network', constraint_name: :constraint_1, dependent: :delete } }
53
52
 
54
53
  it 'normalizes symbols to strings' do
55
- expect(subject.foreign_key).to eq('network_id')
56
- expect(subject.foreign_key_name).to eq('the_network_id')
54
+ expect(subject.foreign_key_column).to eq('network_id')
57
55
  expect(subject.parent_table_name).to eq('networks')
58
56
  expect(subject.constraint_name).to eq('constraint_1')
59
- expect(subject.on_delete_cascade).to be_truthy
57
+ expect(subject.dependent).to eq(:delete)
60
58
  end
61
59
  end
62
60
 
63
61
  describe `#<=>` do
62
+ let(:foreign_key_column) { :the_network_id }
63
+
64
64
  context 'when class name not passed' do
65
- let(:options) { { foreign_key: :the_network_id, constraint_name: :constraint_1, dependent: :delete } }
65
+ let(:options) { { child_table_name: 'advertisers', constraint_name: :constraint_1, dependent: :delete } }
66
66
 
67
- it 'compares equal without requring the parent class' do
67
+ it 'compares equal without requiring the parent class' do
68
68
  expect(subject <=> subject).to eq(0)
69
69
  end
70
70
  end
71
71
 
72
72
  context 'when class name passed' do
73
- let(:options) { { foreign_key: :the_network_id, class_name: 'TheNetwork', constraint_name: :constraint_1 } }
73
+ let(:options) { { child_table_name: 'advertisers', parent_class_name: 'TheNetwork', constraint_name: :constraint_1 } }
74
74
 
75
- it 'compares equal without requring the parent class' do
75
+ it 'compares equal without requiring the parent class' do
76
76
  expect(subject <=> subject).to eq(0)
77
77
  end
78
78
  end
79
79
  end
80
80
 
81
81
  context 'when constraint name passed as empty string' do
82
- let(:options) { { constraint_name: "" } }
82
+ let(:options) { { child_table_name: 'advertisers', constraint_name: "" } }
83
+
83
84
  it 'defaults to rails constraint name' do
84
- expect(subject.constraint_name).to eq("index_on_network_id")
85
+ expect(subject.constraint_name).to eq("index_advertisers_on_network_id")
85
86
  end
86
87
  end
87
88
 
88
89
  context 'when no constraint name passed' do
89
90
  it 'defaults to rails constraint name' do
90
- expect(subject.constraint_name).to eq("index_on_network_id")
91
+ expect(subject.constraint_name).to eq("index_advertisers_on_network_id")
91
92
  end
92
93
  end
93
94
  end
@@ -103,13 +104,13 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
103
104
  allow(connection).to receive(:index_name).with('models', column: 'network_id') { }
104
105
  end
105
106
 
106
- describe '.for_model' do
107
- subject { described_class.for_model(model, old_table_name) }
107
+ describe '.for_table' do
108
+ subject { described_class.for_table(old_table_name, model.connection) }
108
109
 
109
- it 'returns new object' do
110
- expect(subject.size).to eq(1), subject.inspect
111
- expect(subject.first).to be_kind_of(described_class)
112
- expect(subject.first.foreign_key).to eq('network_id')
110
+ it 'returns definitions' do
111
+ expect(subject.map(&:key)).to eq([
112
+ ["networks", "network_id", nil]
113
+ ])
113
114
  end
114
115
  end
115
116
  end