declare_schema 1.4.0.colin.8 → 1.4.0.colin.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca0ed75221c664e2a5c59bff495a971495922f7f31ea0fb548fb67f0ad0eb639
4
- data.tar.gz: fc76669be291fff9da61353f7f040ba14327e1359f712a963e5865ed948034c8
3
+ metadata.gz: 13075fb086e36e6be6818fd28620cb9015c257083e994f68c216ca790e463896
4
+ data.tar.gz: 4668a054c8d87a7612ba739e3937a4faf817b3ac558083c8e0af59668aa1c42a
5
5
  SHA512:
6
- metadata.gz: 47b53d7e5306a3b2ada9296494af897738056c1c5e14e35e0b3967daf3176e07cf48c74560e2ccd659d0c1b8bc466e6d29f632f86b25bff568bcc4b6dcf672af
7
- data.tar.gz: 9af422633cae00b32e94bf64d0e6e9445aee83849e4898633384af679b61c877a22ce14f1b20c225d17f4a1546ee3c3a1867e0661516f186717895a658d4b2eb
6
+ metadata.gz: 86d9f02bb9cabbb23ecec2eb68098b625b753b04890811858e4a43963d111ac54e5d2e10cd5468dfbdc2c6af39d1cab779002b033dbcbc05e869e4f0bc11f151
7
+ data.tar.gz: fbc233f25183279a309a23dc9afb395b4c4a47e52a95661126c99d9a9109100c5bd5ac3b200171fca479bd9c569f1a9aa34dbe15c79e8cfe50a95387f683ab7e
data/CHANGELOG.md CHANGED
@@ -10,7 +10,21 @@ Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0
10
10
  ### Changed
11
11
  - Deprecate index: 'name' and unique: true|false in favor of index: { name: 'name', unique: true|false }.
12
12
 
13
- ## [1.3.3] - 2023-01-17
13
+ ## [1.3.6] - 2024-01-22
14
+ ### Fixed
15
+ - Add missing commits around connection: and Array check for composite declared primary key.
16
+
17
+ ## [1.3.5] - 2024-01-22
18
+ ### Fixed
19
+ - Make `default_charset=` and `default_collation=` lazy so they don't use the database connection to check the
20
+ MySQL version. Instead, that is checked the first time `default_charset` or `default_collation` is called.
21
+
22
+ ## [1.3.4] - 2024-01-18
23
+ ### Fixed
24
+ - Add test for migrating `has_and_belongs_to_many` associations and fix them to properly declare their
25
+ 2 foreign keys as the primary key of the join table, rather than just a unique index.
26
+
27
+ ## [1.3.3] - 2024-01-17
14
28
  ### Fixed
15
29
  - Fix a MySQL 8 bug where MySQL 8+ renames charset 'utf8' to 'utf8mb3' and collation 'utf8_general_ci' to
16
30
  'utf8mb3_unicode_ci'.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (1.4.0.colin.8)
4
+ declare_schema (1.4.0.colin.11)
5
5
  rails (>= 5.0)
6
6
 
7
7
  GEM
@@ -4,15 +4,17 @@ module DeclareSchema
4
4
  module Model
5
5
  class HabtmModelShim
6
6
  class << self
7
- def from_reflection(refl)
8
- new(refl.join_table, [refl.foreign_key, refl.association_foreign_key],
9
- [refl.active_record.table_name, refl.class_name.constantize.table_name])
7
+ def from_reflection(reflection)
8
+ new(reflection.join_table,
9
+ [reflection.foreign_key, reflection.association_foreign_key],
10
+ [reflection.active_record.table_name, reflection.klass.table_name],
11
+ connection: reflection.active_record.connection)
10
12
  end
11
13
  end
12
14
 
13
- attr_reader :join_table, :foreign_keys, :parent_table_names
15
+ attr_reader :join_table, :foreign_keys, :parent_table_names, :connection
14
16
 
15
- def initialize(join_table, foreign_keys, parent_table_names)
17
+ def initialize(join_table, foreign_keys, parent_table_names, connection:)
16
18
  foreign_keys.is_a?(Array) && foreign_keys.size == 2 or
17
19
  raise ArgumentError, "foreign_keys must be <Array[2]>; got #{foreign_keys.inspect}"
18
20
  parent_table_names.is_a?(Array) && parent_table_names.size == 2 or
@@ -20,6 +22,7 @@ module DeclareSchema
20
22
  @join_table = join_table
21
23
  @foreign_keys = foreign_keys.sort # Rails requires these be in alphabetical order
22
24
  @parent_table_names = @foreign_keys == foreign_keys ? parent_table_names : parent_table_names.reverse # match the above sort
25
+ @connection = connection
23
26
  end
24
27
 
25
28
  def _table_options
@@ -41,17 +44,21 @@ module DeclareSchema
41
44
  end
42
45
 
43
46
  def _declared_primary_key
44
- false # no single-column primary key declared
47
+ foreign_keys
45
48
  end
46
49
 
47
- def index_definitions_with_primary_key
48
- @index_definitions_with_primary_key ||= Set.new([
49
- IndexDefinition.new(foreign_keys, name: Model::IndexDefinition::PRIMARY_KEY_NAME, table_name: table_name, unique: true), # creates a primary composite key on both foreign keys
50
- IndexDefinition.new(foreign_keys.last, table_name: table_name, unique: false) # index for queries where we only have the last foreign key
51
- ])
50
+ def index_definitions
51
+ [
52
+ IndexDefinition.new(foreign_keys.last, table_name: table_name, unique: false) # index for queries where we only have the last foreign key
53
+ ]
52
54
  end
53
55
 
54
- alias_method :index_definitions, :index_definitions_with_primary_key
56
+ def index_definitions_with_primary_key
57
+ [
58
+ *index_definitions,
59
+ IndexDefinition.new(foreign_keys, table_name: table_name, name: Model::IndexDefinition::PRIMARY_KEY_NAME, unique: true) # creates a primary composite key on both foreign keys
60
+ ]
61
+ end
55
62
 
56
63
  def ignore_indexes
57
64
  @ignore_indexes ||= Set.new
@@ -16,12 +16,9 @@ module DeclareSchema
16
16
 
17
17
  PRIMARY_KEY_NAME = "PRIMARY"
18
18
 
19
- # Caller needs to pass either name or table_name. The table_name is not remembered; it is just used to compute the
20
- # default name if no name is given.
21
19
  def initialize(columns, table_name:, name: nil, allow_equivalent: false, unique: false, where: nil, length: nil)
22
20
  @table_name = table_name
23
- @name = name || self.class.default_index_name(table_name, columns)
24
- @name.to_s == 'index_adverts_on_Advert' and binding.pry
21
+ @name = (name || self.class.default_index_name(table_name, columns)).to_s
25
22
  @columns = Array.wrap(columns).map(&:to_s)
26
23
  @explicit_name = @name if !allow_equivalent
27
24
  unique.in?([false, true]) or raise ArgumentError, "unique must be true or false: got #{unique.inspect}"
@@ -38,7 +35,7 @@ module DeclareSchema
38
35
  @where = where.start_with?('(') ? where : "(#{where})"
39
36
  end
40
37
 
41
- @length = length
38
+ @length = self.class.normalize_index_length(length, columns: @columns)
42
39
  end
43
40
 
44
41
  class << self
@@ -57,16 +54,7 @@ module DeclareSchema
57
54
  raise "primary key on #{table_name} was not unique on #{primary_key_columns} (was unique=#{index.unique} on #{index.columns})"
58
55
  primary_key_found = true
59
56
  end
60
- length =
61
- case lengths = index.lengths
62
- when {}
63
- nil
64
- when Hash
65
- lengths.size == 1 ? lengths.values.first : lengths
66
- else
67
- lengths
68
- end
69
- new(index.columns, name: index.name, table_name: table_name, unique: index.unique, where: index.where, length: length)
57
+ new(index.columns, name: index.name, table_name: table_name, unique: index.unique, where: index.where, length: index.lengths)
70
58
  end.compact
71
59
 
72
60
  if !primary_key_found
@@ -86,6 +74,26 @@ module DeclareSchema
86
74
  "Default index name '#{index_name}' exceeds configured limit of #{DeclareSchema.max_index_and_constraint_name_length} characters. Use the `name:` option to give it a shorter name, or adjust DeclareSchema.max_index_and_constraint_name_length if you know your database can accept longer names."
87
75
  end
88
76
 
77
+ # This method normalizes the length option to be either nil or a Hash of Symbol column names to lengths,
78
+ # so that we can safely compare what the user specified with what we get when querying the database schema.
79
+ # @return [Hash<Symbol, nil>]
80
+ def normalize_index_length(length, columns:)
81
+ case length
82
+ when nil, {}
83
+ nil
84
+ when Integer
85
+ if columns.size == 1
86
+ { columns.first.to_sym => length }
87
+ else
88
+ raise ArgumentError, "Index length of Integer only allowed when exactly one column; got #{length.inspect} for #{columns.inspect}"
89
+ end
90
+ when Hash
91
+ length.transform_keys(&:to_sym)
92
+ else
93
+ raise ArgumentError, "Index length must be nil or Integer or a Hash of column names to lengths; got #{length.inspect} for #{columns.inspect}"
94
+ end
95
+ end
96
+
89
97
  private
90
98
 
91
99
  SHA_SUFFIX_LENGTH = 4
@@ -50,10 +50,23 @@ module DeclareSchema
50
50
 
51
51
  module ClassMethods
52
52
  def index(columns, name: nil, allow_equivalent: false, unique: false, where: nil, length: nil)
53
- index_definitions << ::DeclareSchema::Model::IndexDefinition.new(
53
+ index_definition = ::DeclareSchema::Model::IndexDefinition.new(
54
54
  columns,
55
55
  name: name, table_name: table_name, allow_equivalent: allow_equivalent, unique: unique, where: where, length: length
56
56
  )
57
+
58
+ if (equivalent = index_definitions.find { index_definition.equivalent?(_1) }) # differs only by name
59
+ if equivalent == index_definition
60
+ # identical is always idempotent
61
+ else
62
+ # equivalent is idempotent iff allow_equivalent: true passed
63
+ allow_equivalent or
64
+ raise ArgumentError, "equivalent index definition found (pass allow_equivalent: true to ignore):\n" \
65
+ "#{index_definition.inspect}\n#{equivalent.inspect}"
66
+ end
67
+ else
68
+ index_definitions << index_definition
69
+ end
57
70
  end
58
71
 
59
72
  def primary_key_index(*columns)
@@ -61,11 +74,13 @@ module DeclareSchema
61
74
  end
62
75
 
63
76
  def constraint(foreign_key_column, parent_table_name: nil, constraint_name: nil, parent_class_name: nil, dependent: nil)
64
- constraint_definitions << ::DeclareSchema::Model::ForeignKeyDefinition.new(
77
+ constraint_definition = ::DeclareSchema::Model::ForeignKeyDefinition.new(
65
78
  foreign_key_column.to_s,
66
79
  constraint_name: constraint_name,
67
80
  child_table_name: table_name, parent_table_name: parent_table_name, parent_class_name: parent_class_name, dependent: dependent
68
81
  )
82
+
83
+ constraint_definitions << constraint_definition # Set<> implements idempotent insert.
69
84
  end
70
85
 
71
86
  # tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes
@@ -111,31 +126,32 @@ module DeclareSchema
111
126
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
112
127
  if options.has_key?(:limit)
113
128
  options.delete(:limit)
114
- ActiveSupport::Deprecation.warn("belongs_to limit: is deprecated since it is now inferred")
129
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, limit: is deprecated since it is now inferred")
115
130
  end
116
131
 
117
132
  # index: true means create an index on the foreign key
118
133
  # index: false means do not create an index on the foreign key
119
134
  # index: { ... } means create an index on the foreign key with the given options
120
135
  index_value = options.delete(:index)
121
- if index_value != false || options.has_key?(:unique) || options.has_key?(:allow_equivalent)
122
- index_options = {} # truthy iff we want an index
136
+ if index_value == false # don't create an index
137
+ options.delete(:unique)
138
+ options.delete(:allow_equivalent)
139
+ else
140
+ index_options = {} # create an index
123
141
  case index_value
124
142
  when String, Symbol
125
- Kernel.warn("belongs_to index: 'name' is deprecated; use index: { name: 'name' } instead (in #{name})")
143
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, index: 'name' is deprecated; use index: { name: 'name' } instead (in #{self.name})")
126
144
  index_options[:name] = index_value.to_s
127
- when false
128
- raise ArgumentError, "belongs_to index: false contradicts others options #{options.inspect} (in #{name})"
129
145
  when true
130
146
  when nil
131
147
  when Hash
132
148
  index_options = index_value
133
149
  else
134
- raise ArgumentError, "belongs_to index: must be true or false or a Hash; got #{index_value.inspect} (in #{name})"
150
+ raise ArgumentError, "belongs_to #{name.inspect}, index: must be true or false or a Hash; got #{index_value.inspect} (in #{self.name})"
135
151
  end
136
152
 
137
153
  if options.has_key?(:unique)
138
- Kernel.warn("belongs_to unique: true|false is deprecated; use index: { unique: true|false } instead (in #{name})")
154
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, unique: true|false is deprecated; use index: { unique: true|false } instead (in #{self.name})")
139
155
  index_options[:unique] = options.delete(:unique)
140
156
  end
141
157
 
@@ -186,7 +202,7 @@ module DeclareSchema
186
202
  end
187
203
 
188
204
  if ::DeclareSchema.default_generate_foreign_keys && constraint_name != false
189
- constraint(foreign_key_column, constraint_name: constraint_name || index_options&.[](:name), parent_class_name: reflection.klass, dependent: dependent_delete)
205
+ constraint(foreign_key_column, constraint_name: constraint_name || index_options&.[](:name), parent_class_name: reflection.class_name, dependent: dependent_delete)
190
206
  end
191
207
  end
192
208
  end
@@ -6,8 +6,10 @@ module DeclareSchema
6
6
  module SchemaChange
7
7
  class ColumnAdd < Base
8
8
  def initialize(table_name, column_name, column_type, **column_options)
9
- @table_name = table_name or raise ArgumentError, "must provide table_name"
10
- @column_name = column_name or raise ArgumentError, "must provide column_name"
9
+ table_name.is_a?(String) || table_name.is_a?(Symbol) or raise ArgumentError, "must provide String|Symbol table_name; got #{table_name.inspect}"
10
+ column_name.is_a?(String) || column_name.is_a?(Symbol) or raise ArgumentError, "must provide String|Symbol column_name; got #{column_name.inspect}"
11
+ @table_name = table_name
12
+ @column_name = column_name
11
13
  @column_type = column_type or raise ArgumentError, "must provide column_type"
12
14
  @column_options = column_options
13
15
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "1.4.0.colin.8"
4
+ VERSION = "1.4.0.colin.11"
5
5
  end
@@ -36,7 +36,7 @@ module DeclareSchema
36
36
 
37
37
  class << self
38
38
  attr_writer :mysql_version
39
- attr_reader :default_charset, :default_collation, :default_text_limit, :default_string_limit, :default_null,
39
+ attr_reader :default_text_limit, :default_string_limit, :default_null,
40
40
  :default_generate_foreign_keys, :default_generate_indexing, :db_migrate_command,
41
41
  :max_index_and_constraint_name_length
42
42
 
@@ -81,12 +81,22 @@ module DeclareSchema
81
81
 
82
82
  def default_charset=(charset)
83
83
  charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
84
- @default_charset = normalize_charset(charset)
84
+ @default_charset_before_normalization = charset
85
+ @default_charset = nil
86
+ end
87
+
88
+ def default_charset
89
+ @default_charset ||= normalize_charset(@default_charset_before_normalization)
85
90
  end
86
91
 
87
92
  def default_collation=(collation)
88
93
  collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
89
- @default_collation = normalize_collation(collation)
94
+ @default_collation_before_normalization = collation
95
+ @default_collation = nil
96
+ end
97
+
98
+ def default_collation
99
+ @default_collation ||= normalize_collation(@default_collation_before_normalization)
90
100
  end
91
101
 
92
102
  def default_text_limit=(text_limit)
@@ -204,8 +204,8 @@ module Generators
204
204
  end
205
205
  end
206
206
  # generate shims for HABTM models
207
- habtm_tables.each do |name, refls|
208
- 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)
209
209
  end
210
210
  model_table_names = models_by_table_name.keys
211
211
 
@@ -222,8 +222,8 @@ module Generators
222
222
  ::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
223
223
  end
224
224
 
225
- creates = to_create.map do |t|
226
- model = models_by_table_name[t]
225
+ creates = to_create.map do |table_name|
226
+ model = models_by_table_name[table_name]
227
227
  disable_auto_increment = model.try(:disable_auto_increment)
228
228
 
229
229
  primary_key_definition =
@@ -240,10 +240,13 @@ module Generators
240
240
  table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, **table_options_for_model(model))
241
241
  table_options = create_table_options(model, disable_auto_increment)
242
242
 
243
- table_add = ::DeclareSchema::SchemaChange::TableAdd.new(t,
244
- primary_key_definition + field_definitions,
245
- table_options,
246
- 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
+
247
250
  [
248
251
  table_add,
249
252
  *Array((create_indexes(model) if ::DeclareSchema.default_generate_indexing)),
@@ -256,9 +259,9 @@ module Generators
256
259
  fk_changes = []
257
260
  table_options_changes = []
258
261
 
259
- to_change.each do |t|
260
- model = models_by_table_name[t]
261
- 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
262
265
  if table.in?(db_tables)
263
266
  change, index_change, fk_change, table_options_change = change_table(model, table)
264
267
  changes << change
@@ -312,6 +315,8 @@ module Generators
312
315
  { id: false }
313
316
  elsif primary_key == "id"
314
317
  { id: :bigint }
318
+ elsif primary_key.is_a?(Array)
319
+ { primary_key: primary_key.map(&:to_sym) }
315
320
  else
316
321
  { primary_key: primary_key.to_sym }
317
322
  end.merge(model._table_options)
@@ -347,7 +352,7 @@ module Generators
347
352
 
348
353
  db_columns = model.connection.columns(current_table_name).index_by(&:name)
349
354
  if (pk = model._declared_primary_key.presence)
350
- pk_was_in_db_columns = db_columns.delete(pk)
355
+ pk_was_in_db_columns = pk.is_a?(Array) || db_columns.delete(pk)
351
356
  end
352
357
 
353
358
  model_column_names = model.field_specs.keys.map(&:to_s)
@@ -414,7 +414,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
414
414
 
415
415
  # You can specify the index name with index: 'name' [deprecated]
416
416
 
417
- 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)
418
418
 
419
419
  class Category < ActiveRecord::Base; end
420
420
  class Advert < ActiveRecord::Base
@@ -487,8 +487,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
487
487
 
488
488
  # You can add an index to a field definition
489
489
 
490
- expect(Kernel).to receive(:warn).with(/belongs_to index: 'name' is deprecated; use index: \{ name: 'name' \} instead/i)
491
- 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)
492
492
 
493
493
  class Advert < ActiveRecord::Base
494
494
  declare_schema do
@@ -572,7 +572,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
572
572
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
573
573
  migrate_up(<<~EOS.strip)
574
574
  add_column :adverts, :title, :string, limit: 250, null: true#{charset_and_collation}
575
- add_index :adverts, [:title], name: :my_index, length: 10
575
+ add_index :adverts, [:title], name: :my_index, length: { title: 10 }
576
576
  EOS
577
577
  )
578
578
 
@@ -820,6 +820,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
820
820
  expect(User.field_specs.keys).to eq(['company'])
821
821
  expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
822
822
 
823
+ nuke_model_class(User)
824
+
823
825
  ## validates
824
826
 
825
827
  # DeclareSchema can accept a validates hash in the field options.
@@ -840,6 +842,44 @@ RSpec.describe 'DeclareSchema Migration Generator' do
840
842
  up, _down = Generators::DeclareSchema::Migration::Migrator.run
841
843
  ActiveRecord::Migration.class_eval(up)
842
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)
843
883
  end
844
884
 
845
885
  context 'models with the same parent foreign key relation' do
@@ -863,7 +903,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
863
903
  end
864
904
  end
865
905
 
866
- it 'will genereate unique constraint names' do
906
+ it 'will generate unique constraint names' do
867
907
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
868
908
  migrate_up(<<~EOS.strip)
869
909
  create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
@@ -1160,7 +1200,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1160
1200
  end
1161
1201
 
1162
1202
  it 'deprecates limit:' do
1163
- 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")
1164
1204
  eval <<~EOS
1165
1205
  class UsingLimit < ActiveRecord::Base
1166
1206
  declare_schema { }
@@ -1397,5 +1437,53 @@ RSpec.describe 'DeclareSchema Migration Generator' do
1397
1437
  end
1398
1438
  end
1399
1439
  end
1440
+
1441
+ context 'index' do
1442
+ before do
1443
+ class Advert < active_record_base_class.constantize
1444
+ declare_schema { }
1445
+ belongs_to :ad_category
1446
+ end
1447
+ end
1448
+
1449
+ it "is idempotent and doesn't raise" do
1450
+ expect do
1451
+ Advert.index [:ad_category_id], name: :index_adverts_on_ad_category_id
1452
+ end.to_not change { Advert.index_definitions.size }
1453
+ end
1454
+
1455
+ it "when equivalent but not marked to allow, it raises" do
1456
+ expect do
1457
+ Advert.index [:ad_category_id], name: :on_ad_category_id
1458
+ end.to raise_exception(ArgumentError, /equivalent index definition found/i)
1459
+ end
1460
+
1461
+ it "when equivalent and marked to allow, it is idempotent and doesn't raise" do
1462
+ expect do
1463
+ Advert.index [:ad_category_id], name: :on_ad_category_id, allow_equivalent: true
1464
+ end.to_not change { Advert.index_definitions.size }
1465
+ end
1466
+
1467
+ context 'constraint' do
1468
+ before do
1469
+ class Advert < active_record_base_class.constantize
1470
+ declare_schema { }
1471
+ belongs_to :ad_category
1472
+ end
1473
+ end
1474
+
1475
+ it "when exactly equal, it is idempotent and doesn't raise" do
1476
+ expect do
1477
+ Advert.constraint :ad_category_id, parent_table_name: 'ad_categories', constraint_name: :index_adverts_on_ad_category_id, parent_class_name: 'AdCategory'
1478
+ end.to_not change { Advert.index_definitions.size }
1479
+ end
1480
+
1481
+ it "when equivalent, it is idempotent and doesn't raise" do
1482
+ expect do
1483
+ Advert.constraint :ad_category_id, parent_table_name: 'ad_categories', constraint_name: :constraint_1, parent_class_name: 'AdCategory'
1484
+ end.to_not change { Advert.index_definitions.size }
1485
+ end
1486
+ end
1487
+ end
1400
1488
  end
1401
1489
  end
@@ -11,6 +11,7 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
11
11
  let(:join_table) { "customers_users" }
12
12
  let(:foreign_keys) { ["user_id", "customer_id"] }
13
13
  let(:parent_table_names) { ["users", "customers"] }
14
+ let(:connection) { instance_double(ActiveRecord::Base.connection.class, "connection") }
14
15
 
15
16
  before do
16
17
  load File.expand_path('../prepare_testapp.rb', __dir__)
@@ -30,8 +31,11 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
30
31
  foreign_key: foreign_keys.first,
31
32
  association_foreign_key: foreign_keys.last,
32
33
  active_record: User,
33
- class_name: 'Customer') }
34
+ class_name: 'Customer',
35
+ klass: Customer) }
34
36
  it 'returns a new object' do
37
+ expect(User).to receive(:connection).and_return(connection)
38
+
35
39
  result = described_class.from_reflection(reflection)
36
40
 
37
41
  expect(result).to be_a(described_class)
@@ -42,9 +46,7 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
42
46
  end
43
47
 
44
48
  describe 'instance methods' do
45
- let(:connection) { instance_double(ActiveRecord::Base.connection.class, "connection") }
46
-
47
- subject { described_class.new(join_table, foreign_keys, parent_table_names) }
49
+ subject { described_class.new(join_table, foreign_keys, parent_table_names, connection: connection) }
48
50
 
49
51
  describe '#initialize' do
50
52
  it 'stores initialization attributes' do
@@ -53,6 +55,12 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
53
55
  end
54
56
  end
55
57
 
58
+ describe '#connection' do
59
+ it 'returns the connection' do
60
+ expect(subject.connection).to be(connection)
61
+ end
62
+ end
63
+
56
64
  describe '#table_options' do
57
65
  it 'returns empty hash' do
58
66
  expect(subject._table_options).to eq({})
@@ -85,14 +93,14 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
85
93
  end
86
94
 
87
95
  describe '#primary_key' do
88
- it 'returns false' do
89
- expect(subject._declared_primary_key).to eq(false)
96
+ it 'returns false because there is no single-column PK for ActiveRecord to use' do
97
+ expect(subject.primary_key).to eq(false)
90
98
  end
91
99
  end
92
100
 
93
101
  describe '#_declared_primary_key' do
94
- it 'returns false' do
95
- expect(subject._declared_primary_key).to eq(false)
102
+ it 'returns the foreign key pair that are used as the primary key in the database' do
103
+ expect(subject._declared_primary_key).to eq(["customer_id", "user_id"])
96
104
  end
97
105
  end
98
106
 
@@ -101,10 +109,10 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
101
109
  index_definitions = subject.index_definitions_with_primary_key
102
110
  expect(index_definitions.size).to eq(2), index_definitions.inspect
103
111
 
104
- expect(index_definitions.first).to be_a(::DeclareSchema::Model::IndexDefinition)
105
- expect(index_definitions.first.name).to eq('PRIMARY')
106
- expect(index_definitions.first.fields).to eq(foreign_keys.reverse)
107
- expect(index_definitions.first.unique).to be_truthy
112
+ expect(index_definitions.last).to be_a(::DeclareSchema::Model::IndexDefinition)
113
+ expect(index_definitions.last.name).to eq('PRIMARY')
114
+ expect(index_definitions.last.fields).to eq(foreign_keys.reverse)
115
+ expect(index_definitions.last.unique).to be_truthy
108
116
  end
109
117
  end
110
118
 
@@ -127,21 +135,38 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
127
135
  it 'returns two index definitions and does not raise a IndexNameTooLongError' do
128
136
  indexes = subject.index_definitions_with_primary_key.to_a
129
137
  expect(indexes.size).to eq(2), indexes.inspect
130
- expect(indexes.first).to be_a(::DeclareSchema::Model::IndexDefinition)
131
- expect(indexes.first.name).to eq('PRIMARY')
132
- expect(indexes.first.fields).to eq(foreign_keys)
133
- expect(indexes.first.unique).to be_truthy
134
138
  expect(indexes.last).to be_a(::DeclareSchema::Model::IndexDefinition)
135
- expect(indexes.last.name).to eq('index_advertiser_campaigns_tracking_pixels_on_campaign_id')
136
- expect(indexes.last.fields).to eq([foreign_keys.last])
137
- expect(indexes.last.unique).to be_falsey
139
+ expect(indexes.last.name).to eq('PRIMARY')
140
+ expect(indexes.last.fields).to eq(foreign_keys)
141
+ expect(indexes.last.unique).to be_truthy
142
+ expect(indexes.first).to be_a(::DeclareSchema::Model::IndexDefinition)
143
+ expect(indexes.first.name).to eq('index_advertiser_campaigns_tracking_pixels_on_campaign_id')
144
+ expect(indexes.first.fields).to eq([foreign_keys.last])
145
+ expect(indexes.first.unique).to be_falsey
138
146
  end
139
147
  end
140
148
 
141
149
  describe '#index_definitions' do
142
- it 'returns index_definitions_with_primary_key' do
150
+ it 'returns index_definitions' do
143
151
  indexes = subject.index_definitions
152
+ expect(indexes.size).to eq(1), indexes.inspect
153
+ expect(indexes.first.columns).to eq(["user_id"])
154
+ options = [:name, :unique, :where].map { |k| [k, indexes.first.send(k)] }.to_h
155
+ expect(options).to eq(name: "index_customers_users_on_user_id",
156
+ unique: false,
157
+ where: nil)
158
+ end
159
+ end
160
+
161
+ describe '#index_definitions_with_primary_key' do
162
+ it 'returns index_definitions_with_primary_key' do
163
+ indexes = subject.index_definitions_with_primary_key
144
164
  expect(indexes.size).to eq(2), indexes.inspect
165
+ expect(indexes.last.columns).to eq(["customer_id", "user_id"])
166
+ options = [:name, :unique, :where].map { |k| [k, indexes.last.send(k)] }.to_h
167
+ expect(options).to eq(name: "PRIMARY",
168
+ unique: true,
169
+ where: nil)
145
170
  end
146
171
  end
147
172
 
@@ -77,13 +77,14 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
77
77
  let(:options) { { table_name: table_name, length: length } }
78
78
 
79
79
  context 'with integer length' do
80
+ let(:fields) { ['last_name'] }
80
81
  let(:length) { 2 }
81
82
 
82
- it { is_expected.to eq(length) }
83
+ it { is_expected.to eq(last_name: 2) }
83
84
  end
84
85
 
85
86
  context 'with Hash length' do
86
- let(:length) { { name: 2 } }
87
+ let(:length) { { first_name: 2 } }
87
88
 
88
89
  it { is_expected.to eq(length) }
89
90
  end
@@ -91,7 +92,7 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
91
92
 
92
93
  describe '#options' do
93
94
  subject { instance.options }
94
- let(:options) { { name: 'my_index', table_name: table_name, unique: false, where: "(name like 'a%')", length: 10 } }
95
+ let(:options) { { name: 'my_index', table_name: table_name, unique: false, where: "(last_name like 'a%')", length: { last_name: 10, first_name: 5 } } }
95
96
 
96
97
  it { is_expected.to eq(options.except(:table_name)) }
97
98
  end
@@ -163,7 +164,7 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
163
164
  it 'returns the indexes for the model' do
164
165
  expect(subject.map(&:to_key)).to eq([
165
166
  ["index_definition_test_models_on_name", ["name"], { unique: true, where: nil, length: nil }],
166
- (["index_definition_test_models_on_name_partial", ["name"], { unique: false, where: nil, length: 10 }] if defined?(Mysql2)),
167
+ (["index_definition_test_models_on_name_partial", ["name"], { unique: false, where: nil, length: { name: 10 } }] if defined?(Mysql2)),
167
168
  ["PRIMARY", ["id"], { unique: true, where: nil, length: nil }]
168
169
  ].compact)
169
170
  end
@@ -184,7 +185,7 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
184
185
 
185
186
  it 'skips the ignored index' do
186
187
  expect(subject.map(&:to_key)).to eq([
187
- (["index_definition_test_models_on_name_partial", ["name"], { unique: false, where: nil, length: 10 }] if defined?(Mysql2)),
188
+ (["index_definition_test_models_on_name_partial", ["name"], { unique: false, where: nil, length: { name: 10 } }] if defined?(Mysql2)),
188
189
  ["PRIMARY", ["id"], { length: nil, unique: true, where: nil }]
189
190
  ].compact)
190
191
  end
@@ -259,6 +260,69 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
259
260
  end
260
261
  end
261
262
  end
263
+
264
+ describe '.normalize_index_length' do
265
+ let(:columns) { [:last_name] }
266
+ subject { described_class.normalize_index_length(length, columns: columns) }
267
+
268
+ context 'with nil length' do
269
+ let(:length) { nil }
270
+
271
+ it { is_expected.to eq(nil) }
272
+ end
273
+
274
+ context 'when Integer' do
275
+ let(:length) { 10 }
276
+
277
+ it { is_expected.to eq(last_name: length) }
278
+
279
+ context 'with multiple columns' do
280
+ let(:columns) { ["last_name", "first_name"] }
281
+
282
+ it { expect { subject }.to raise_exception(ArgumentError, /Index length of Integer only allowed when exactly one column; got 10 for \["last_name", "first_name"]/i) }
283
+ end
284
+ end
285
+
286
+ context 'when empty Hash' do
287
+ let(:length) { {} }
288
+
289
+ it { is_expected.to eq(nil) }
290
+ end
291
+
292
+ context 'when Hash' do
293
+ let(:length) { { last_name: 10 } }
294
+
295
+ it { is_expected.to eq(length) }
296
+ end
297
+
298
+ context 'when Hash with String key' do
299
+ let(:length) { { "last_name" => 10 } }
300
+
301
+ it { is_expected.to eq(last_name: 10) }
302
+ end
303
+
304
+ context 'with multiple columns' do
305
+ let(:columns) { [:last_name, :first_name] }
306
+
307
+ context 'when Hash with String keys' do
308
+ let(:length) { { "last_name" => 10, "first_name" => 5 } }
309
+
310
+ it { is_expected.to eq(last_name: 10, first_name: 5) }
311
+ end
312
+ end
313
+
314
+ context 'with nil length' do
315
+ let(:length) { nil }
316
+
317
+ it { is_expected.to eq(nil) }
318
+ end
319
+
320
+ context 'with an invalid length' do
321
+ let(:length) { 10.5 }
322
+
323
+ it { expect { subject }.to raise_exception(ArgumentError, /length must be nil or Integer or a Hash of column names to lengths; got 10\.5 for \[:last_name]/i) }
324
+ end
325
+ end
262
326
  end
263
327
  # TODO: fill out remaining tests
264
328
  end
@@ -37,6 +37,26 @@ RSpec.describe DeclareSchema do
37
37
  it { is_expected.to eq("utf8mb3") }
38
38
  end
39
39
  end
40
+
41
+ context 'when MySQL version not known yet' do
42
+ before { described_class.remove_instance_variable('@mysql_version') rescue nil }
43
+ after { described_class.remove_instance_variable('@mysql_version') rescue nil }
44
+
45
+ context 'when set' do
46
+ let(:connection) { double("connection", select_value: "8.0.21") }
47
+
48
+ it "is lazy, so it doesn't use the database connection until read" do
49
+ expect(ActiveRecord::Base).to receive(:connection) do
50
+ @connection_called = true
51
+ connection
52
+ end
53
+ described_class.default_charset = "utf8"
54
+ expect(@connection_called).to be_falsey
55
+ described_class.default_charset
56
+ expect(@connection_called).to be_truthy
57
+ end
58
+ end
59
+ end
40
60
  end
41
61
 
42
62
  describe '#default_collation' do
@@ -81,6 +101,26 @@ RSpec.describe DeclareSchema do
81
101
  it { is_expected.to eq("utf8mb3_general_ci") }
82
102
  end
83
103
  end
104
+
105
+ context 'when MySQL version not known yet' do
106
+ before { described_class.remove_instance_variable('@mysql_version') rescue nil }
107
+ after { described_class.remove_instance_variable('@mysql_version') rescue nil }
108
+
109
+ context 'when set' do
110
+ let(:connection) { double("connection", select_value: "8.0.21") }
111
+
112
+ it "is lazy, so it doesn't use the database connection until read" do
113
+ expect(ActiveRecord::Base).to receive(:connection) do
114
+ @connection_called = true
115
+ connection
116
+ end
117
+ described_class.default_collation = "utf8_general_ci"
118
+ expect(@connection_called).to be_falsey
119
+ described_class.default_collation
120
+ expect(@connection_called).to be_truthy
121
+ end
122
+ end
123
+ end
84
124
  end
85
125
 
86
126
  describe '#default_text_limit' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: declare_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0.colin.8
4
+ version: 1.4.0.colin.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca Development adapted from hobo_fields by Tom Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-18 00:00:00.000000000 Z
11
+ date: 2024-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails