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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f8226540260a887af839da5afcb4641217c03ac5a6eb2d7a00da2806188f1ce
4
- data.tar.gz: 7eeb464b5bfc9b1fbf88446696ede88b8ea582060658421cf2772e62a6205797
3
+ metadata.gz: 7e20de5eeec8ea9436d2fd674f5ff33f20888cfb87e17456fd278a38504bbede
4
+ data.tar.gz: 259d8dd463292836e40d3fe07261743e714d588440d8a988183daa32f0c8a4d9
5
5
  SHA512:
6
- metadata.gz: 2294e94f2165ec156807fbf9ba98dea10f191bfda1b4c630f5444ab08ee7f80323e86a9af80e968e20e579e21ba24ddef520312a7f0a146d850b6f8927aa50aa
7
- data.tar.gz: 2742ce98af1fb6ef8eb4ba1fa40563a635abb584cc1860efd373a474c6dce9fc1a27b630141f94ea6a23f36602d8381b08d6a33b13b6fd3d352b40df85f37daf
6
+ metadata.gz: 18dc843b60c81ace18bc692f1e8d76038d50695081ab846b14f0653aa4d6a755666417f4d7190429aa83e9eb9fffba02d61c71b2a5c44ef7dbf69baf401e7e31
7
+ data.tar.gz: 94bf1af85ecdc252e08f6453018edc6a2f216e65aa6d7fce0ab7399058097d6a0e1a834e02d350789e8e6fb3485050979dde543e2036e048cc914dd6ffb5cdad
data/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@ 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.4] - 2024-01-18
14
+ ### Fixed
15
+ - Add test for migrating `has_and_belongs_to_many` associations and fix them to properly declare their
16
+ 2 foreign keys as the primary key of the join table, rather than just a unique index.
17
+
18
+ ## [1.3.3] - 2024-01-17
19
+ ### Fixed
20
+ - Fix a MySQL 8 bug where MySQL 8+ renames charset 'utf8' to 'utf8mb3' and collation 'utf8_general_ci' to
21
+ 'utf8mb3_unicode_ci'.
22
+
13
23
  ## [1.3.2] - 2024-01-12
14
24
  ### Fixed
15
25
  - Fix bug in migrator when table option definitions differ
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (1.4.0.colin.7)
4
+ declare_schema (1.4.0.colin.9)
5
5
  rails (>= 5.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -176,7 +176,7 @@ DeclareSchema.clear_default_schema
176
176
  ```
177
177
 
178
178
  ### Global Configuration
179
- Configurations can be set at globally to customize default declaration for the following values:
179
+ Configurations can be set globally to customize default declaration for the following values:
180
180
 
181
181
  #### Text Limit
182
182
  The default text limit can be set using the `DeclareSchema.default_text_limit=` method.
@@ -256,6 +256,9 @@ turn all tables into `utf8mb4` supporting tables:
256
256
  DeclareSchema.default_charset = "utf8mb4"
257
257
  DeclareSchema.default_collation = "utf8mb4_bin"
258
258
  ```
259
+ Note: MySQL 8+ aliases charset 'utf8' to 'utf8mb3', and 'utf8_general_ci' to 'utf8mb3_unicode_ci',
260
+ so when running on MySQL 8+, those aliases will be applied by `DeclareSchema`.
261
+
259
262
  #### db:migrate Command
260
263
  `declare_schema` can run the migration once it is generated, if the `--migrate` option is passed.
261
264
  If not, it will display the command to run later. By default this command is
@@ -107,7 +107,9 @@ module DeclareSchema
107
107
  if @type.in?([:text, :string])
108
108
  if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
109
109
  @options[:charset] ||= model._table_options&.[](:charset) || ::DeclareSchema.default_charset
110
+ @options[:charset] = DeclareSchema.normalize_charset(@options[:charset])
110
111
  @options[:collation] ||= model._table_options&.[](:collation) || ::DeclareSchema.default_collation
112
+ @options[:collation] = DeclareSchema.normalize_collation(@options[:collation])
111
113
  else
112
114
  @options.delete(:charset)
113
115
  @options.delete(:collation)
@@ -7,53 +7,61 @@ module DeclareSchema
7
7
  class ForeignKeyDefinition
8
8
  include Comparable
9
9
 
10
- attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :child_table_name, :options, :on_delete_cascade
11
-
12
-
13
- def initialize(model, foreign_key, **options)
14
- @model = model
15
- @foreign_key = foreign_key.to_s.presence or raise ArgumentError "Foreign key must not be empty: #{foreign_key.inspect}"
16
- @options = options
17
-
18
- @child_table_name = model.table_name # unless a table rename, which would happen when a class is renamed??
19
- @parent_table_name = options[:parent_table]&.to_s
20
- @foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
21
-
10
+ attr_reader :foreign_key_column, :constraint_name, :child_table_name, :parent_class_name, :dependent
11
+
12
+ # Caller needs to pass either constraint_name or child_table_name, and
13
+ # either parent_class_name or parent_table_name.
14
+ def initialize(foreign_key_column, constraint_name: nil, child_table_name: nil, parent_class_name: nil, parent_table_name: nil, dependent: nil)
15
+ @foreign_key_column = foreign_key_column&.to_s or raise ArgumentError "foreign key must not be empty: #{foreign_key_column.inspect}"
16
+ @constraint_name = constraint_name&.to_s.presence || ::DeclareSchema::Model::IndexDefinition.default_index_name(child_table_name, [@foreign_key_column])
17
+ @child_table_name = child_table_name&.to_s or raise ArgumentError, "child_table_name must not be nil"
22
18
  @parent_class_name =
23
- case class_name = options[:class_name]
19
+ case parent_class_name
24
20
  when String, Symbol
25
- class_name.to_s
21
+ parent_class_name.to_s
26
22
  when Class
27
- @parent_class = class_name
23
+ @parent_class = parent_class_name
28
24
  @parent_class.name
29
25
  when nil
30
- @foreign_key.sub(/_id\z/, '').camelize
26
+ @foreign_key_column.sub(/_id\z/, '').camelize
31
27
  end
32
-
33
- @constraint_name = options[:constraint_name]&.to_s.presence ||
34
- model.connection.index_name(model.table_name, column: @foreign_key_name)
35
- @on_delete_cascade = options[:dependent] == :delete
28
+ @parent_table_name = parent_table_name
29
+ dependent.in?([nil, :delete]) or raise ArgumentError, "dependent: must be nil or :delete"
30
+ @dependent = dependent
36
31
  end
37
32
 
38
33
  class << self
39
- def for_model(model, old_table_name)
40
- show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
34
+ def for_table(child_table_name, connection, dependent: nil)
35
+ show_create_table = connection.select_rows("show create table #{connection.quote_table_name(child_table_name)}").first.last
41
36
  constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
42
37
 
43
38
  constraints.map do |fkc|
44
- name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
45
- options = {
46
- constraint_name: name,
47
- parent_table: parent_table,
48
- foreign_key: foreign_key
49
- }
50
- options[:dependent] = :delete if fkc['ON DELETE CASCADE'] || model.is_a?(DeclareSchema::Model::HabtmModelShim)
51
-
52
- new(model, foreign_key, **options)
39
+ constraint_name, foreign_key_column, parent_table_name = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
40
+ dependent_value = :delete if dependent || fkc['ON DELETE CASCADE']
41
+
42
+ new(foreign_key_column,
43
+ constraint_name: constraint_name,
44
+ child_table_name: child_table_name,
45
+ parent_table_name: parent_table_name,
46
+ dependent: dependent_value)
53
47
  end
54
48
  end
55
49
  end
56
50
 
51
+ def key
52
+ @key ||= [@child_table_name, @foreign_key_column, @dependent].freeze
53
+ end
54
+
55
+ def <=>(rhs)
56
+ key <=> rhs.key
57
+ end
58
+
59
+ alias eql? ==
60
+
61
+ def equivalent?(rhs)
62
+ self == rhs
63
+ end
64
+
57
65
  # returns the parent class as a Class object
58
66
  # lazy loaded so that we don't require the parent class until we need it
59
67
  def parent_class
@@ -64,21 +72,9 @@ module DeclareSchema
64
72
  @parent_table_name ||= parent_class.table_name
65
73
  end
66
74
 
67
- def <=>(rhs)
68
- key <=> rhs.send(:key)
69
- end
70
-
71
- alias eql? ==
72
-
73
75
  def hash
74
76
  key.hash
75
77
  end
76
-
77
- private
78
-
79
- def key
80
- @key ||= [@child_table_name, @parent_class_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
81
- end
82
78
  end
83
79
  end
84
80
  end
@@ -4,28 +4,24 @@ module DeclareSchema
4
4
  module Model
5
5
  class HabtmModelShim
6
6
  class << self
7
- def from_reflection(refl)
8
- join_table = refl.join_table
9
- foreign_keys_and_classes = [
10
- [refl.foreign_key.to_s, refl.active_record],
11
- [refl.association_foreign_key.to_s, refl.class_name.constantize]
12
- ].sort { |a, b| a.first <=> b.first }
13
- foreign_keys = foreign_keys_and_classes.map(&:first)
14
- foreign_key_classes = foreign_keys_and_classes.map(&:last)
15
- # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
16
- # figure that anybody who sets THAT up can deal with their own migrations...
17
- connection = refl.active_record.connection
18
-
19
- new(join_table, foreign_keys, foreign_key_classes, connection)
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)
20
12
  end
21
13
  end
22
14
 
23
- attr_reader :join_table, :foreign_keys, :foreign_key_classes, :connection
15
+ attr_reader :join_table, :foreign_keys, :parent_table_names, :connection
24
16
 
25
- def initialize(join_table, foreign_keys, foreign_key_classes, connection)
17
+ def initialize(join_table, foreign_keys, parent_table_names, connection:)
18
+ foreign_keys.is_a?(Array) && foreign_keys.size == 2 or
19
+ raise ArgumentError, "foreign_keys must be <Array[2]>; got #{foreign_keys.inspect}"
20
+ parent_table_names.is_a?(Array) && parent_table_names.size == 2 or
21
+ raise ArgumentError, "parent_table_names must be <Array[2]>; got #{parent_table_names.inspect}"
26
22
  @join_table = join_table
27
- @foreign_keys = foreign_keys
28
- @foreign_key_classes = foreign_key_classes
23
+ @foreign_keys = foreign_keys.sort # Rails requires these be in alphabetical order
24
+ @parent_table_names = @foreign_keys == foreign_keys ? parent_table_names : parent_table_names.reverse # match the above sort
29
25
  @connection = connection
30
26
  end
31
27
 
@@ -38,8 +34,8 @@ module DeclareSchema
38
34
  end
39
35
 
40
36
  def field_specs
41
- foreign_keys.each_with_index.each_with_object({}) do |(v, position), result|
42
- result[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :bigint, position: position, null: false)
37
+ foreign_keys.each_with_index.each_with_object({}) do |(foreign_key, i), result|
38
+ result[foreign_key] = ::DeclareSchema::Model::FieldSpec.new(self, foreign_key, :bigint, position: i, null: false)
43
39
  end
44
40
  end
45
41
 
@@ -48,26 +44,30 @@ module DeclareSchema
48
44
  end
49
45
 
50
46
  def _declared_primary_key
51
- false # no single-column primary key declared
47
+ foreign_keys
52
48
  end
53
49
 
54
- def index_definitions_with_primary_key
55
- @index_definitions_with_primary_key ||= Set.new([
56
- IndexDefinition.new(self, foreign_keys, unique: true, name: Model::IndexDefinition::PRIMARY_KEY_NAME), # creates a primary composite key on both foreign keys
57
- IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
58
- ])
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
+ ]
59
54
  end
60
55
 
61
- 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
62
62
 
63
63
  def ignore_indexes
64
64
  @ignore_indexes ||= Set.new
65
65
  end
66
66
 
67
- def constraint_specs
68
- @constraint_specs ||= Set.new([
69
- ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
70
- ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
67
+ def constraint_definitions
68
+ @constraint_definitions ||= Set.new([
69
+ ForeignKeyDefinition.new(foreign_keys.first, constraint_name: "#{join_table}_FK1", child_table_name: @join_table, parent_table_name: parent_table_names.first, dependent: :delete),
70
+ ForeignKeyDefinition.new(foreign_keys.last, constraint_name: "#{join_table}_FK2", child_table_name: @join_table, parent_table_name: parent_table_names.last, dependent: :delete)
71
71
  ])
72
72
  end
73
73
  end
@@ -7,65 +7,66 @@ module DeclareSchema
7
7
  class IndexDefinition
8
8
  include Comparable
9
9
 
10
- # TODO: replace `fields` with `columns` and remove alias. -Colin
11
10
  OPTIONS = [:name, :unique, :where, :length].freeze
12
- attr_reader :table, :fields, :explicit_name, *OPTIONS
13
- alias columns fields
11
+ attr_reader :columns, :explicit_name, :table_name, *OPTIONS
12
+
13
+ alias fields columns # TODO: change callers to use columns. -Colin
14
14
 
15
15
  class IndexNameTooLongError < RuntimeError; end
16
16
 
17
17
  PRIMARY_KEY_NAME = "PRIMARY"
18
18
 
19
- def initialize(model, fields, **options)
20
- @model = model
21
- @table = options.delete(:table_name) || model.table_name
22
- @fields = Array.wrap(fields).map(&:to_s)
23
- @explicit_name = options[:name] unless options.delete(:allow_equivalent)
24
- @name = options.delete(:name) || self.class.default_index_name(@table, @fields)
25
- @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
26
- @length = options.delete(:length)
19
+ def initialize(columns, table_name:, name: nil, allow_equivalent: false, unique: false, where: nil, length: nil)
20
+ @table_name = table_name
21
+ @name = name || self.class.default_index_name(table_name, columns)
22
+ @columns = Array.wrap(columns).map(&:to_s)
23
+ @explicit_name = @name if !allow_equivalent
24
+ unique.in?([false, true]) or raise ArgumentError, "unique must be true or false: got #{unique.inspect}"
25
+ if @name == PRIMARY_KEY_NAME
26
+ unique or raise ArgumentError, "primary key index must be unique"
27
+ end
28
+ @unique = unique
27
29
 
28
30
  if DeclareSchema.max_index_and_constraint_name_length && @name.length > DeclareSchema.max_index_and_constraint_name_length
29
31
  raise IndexNameTooLongError, "Index '#{@name}' exceeds configured limit of #{DeclareSchema.max_index_and_constraint_name_length} characters. Give it a shorter name, or adjust DeclareSchema.max_index_and_constraint_name_length if you know your database can accept longer names."
30
32
  end
31
33
 
32
- if (where = options.delete(:where))
34
+ if where
33
35
  @where = where.start_with?('(') ? where : "(#{where})"
34
36
  end
35
37
 
36
- options.any? and warn("ignoring unrecognized option(s): #{options.inspect} for model #{model}")
38
+ @length = self.class.normalize_index_length(length, columns: @columns)
37
39
  end
38
40
 
39
41
  class << self
40
42
  # extract IndexSpecs from an existing table
41
43
  # includes the PRIMARY KEY index
42
- def for_model(model, old_table_name = nil)
43
- t = old_table_name || model.table_name
44
-
45
- primary_key_columns = Array(model.connection.primary_key(t)).presence
46
- primary_key_columns or raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
44
+ def for_table(table_name, ignore_indexes, connection)
45
+ primary_key_columns = Array(connection.primary_key(table_name))
46
+ primary_key_columns.present? or raise "could not find primary key for table #{table_name} in #{connection.columns(table_name).inspect}"
47
47
 
48
48
  primary_key_found = false
49
- index_definitions = model.connection.indexes(t).map do |i|
50
- model.ignore_indexes.include?(i.name) and next
51
- if i.name == PRIMARY_KEY_NAME
52
- i.columns == primary_key_columns && i.unique or
53
- raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
49
+ index_definitions = connection.indexes(table_name).map do |index|
50
+ next if ignore_indexes.include?(index.name)
51
+
52
+ if index.name == PRIMARY_KEY_NAME
53
+ index.columns == primary_key_columns && index.unique or
54
+ raise "primary key on #{table_name} was not unique on #{primary_key_columns} (was unique=#{index.unique} on #{index.columns})"
54
55
  primary_key_found = true
55
56
  end
56
- new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
57
+ new(index.columns, name: index.name, table_name: table_name, unique: index.unique, where: index.where, length: index.lengths)
57
58
  end.compact
58
59
 
59
60
  if !primary_key_found
60
- index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
61
+ index_definitions << new(primary_key_columns, name: PRIMARY_KEY_NAME, table_name: table_name, unique: true)
61
62
  end
62
63
  index_definitions
63
64
  end
64
65
 
65
- def default_index_name(table, fields)
66
+ def default_index_name(table_name, columns)
66
67
  index_name = nil
67
68
  [:long_index_name, :short_index_name].find do |method_name|
68
- index_name = send(method_name, table, fields)
69
+ index_name = send(method_name, table_name, columns)
69
70
  if DeclareSchema.max_index_and_constraint_name_length.nil? || index_name.length <= DeclareSchema.max_index_and_constraint_name_length
70
71
  break index_name
71
72
  end
@@ -73,6 +74,26 @@ module DeclareSchema
73
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."
74
75
  end
75
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
+
76
97
  private
77
98
 
78
99
  SHA_SUFFIX_LENGTH = 4
@@ -115,12 +136,12 @@ module DeclareSchema
115
136
 
116
137
  # Unique key for this object. Used for equality checking.
117
138
  def to_key
118
- @key ||= [table, fields, options].freeze
139
+ @to_key ||= [name, *settings].freeze
119
140
  end
120
141
 
121
142
  # The index settings for this object. Used for equivalence checking. Does not include the name.
122
143
  def settings
123
- @settings ||= [table, fields, options.except(:name)].freeze
144
+ @settings ||= [columns, options.except(:name)].freeze
124
145
  end
125
146
 
126
147
  def hash
@@ -136,7 +157,7 @@ module DeclareSchema
136
157
  end
137
158
 
138
159
  def with_name(new_name)
139
- self.class.new(@model, @fields, **{ **options, name: new_name })
160
+ self.class.new(@columns, name: new_name, table_name: @table_name, unique: @unique, allow_equivalent: @explicit_name.nil?, where: @where, length: @length)
140
161
  end
141
162
 
142
163
  alias eql? ==
@@ -50,7 +50,17 @@ module DeclareSchema
50
50
 
51
51
  def initialize(table_name, **table_options)
52
52
  @table_name = table_name
53
- @table_options = table_options
53
+ @table_options = table_options.each_with_object({}) do |(k, v),result|
54
+ result[k] =
55
+ case k
56
+ when :charset
57
+ DeclareSchema.normalize_charset(v)
58
+ when :collation
59
+ DeclareSchema.normalize_collation(v)
60
+ else
61
+ v
62
+ end
63
+ end
54
64
  end
55
65
 
56
66
  def to_key
@@ -28,7 +28,7 @@ module DeclareSchema
28
28
  # index_definitions holds IndexDefinition objects for all the declared indexes.
29
29
  inheriting_cattr_reader index_definitions: Set.new
30
30
  inheriting_cattr_reader ignore_indexes: Set.new
31
- inheriting_cattr_reader constraint_specs: Set.new
31
+ inheriting_cattr_reader constraint_definitions: Set.new
32
32
 
33
33
  # table_options holds optional configuration for the create_table statement
34
34
  # supported options include :charset and :collation
@@ -49,16 +49,23 @@ module DeclareSchema
49
49
  end
50
50
 
51
51
  module ClassMethods
52
- def index(fields, **options)
53
- index_definitions << ::DeclareSchema::Model::IndexDefinition.new(self, fields, **options)
52
+ def index(columns, name: nil, allow_equivalent: false, unique: false, where: nil, length: nil)
53
+ index_definitions << ::DeclareSchema::Model::IndexDefinition.new(
54
+ columns,
55
+ name: name, table_name: table_name, allow_equivalent: allow_equivalent, unique: unique, where: where, length: length
56
+ )
54
57
  end
55
58
 
56
- def primary_key_index(*fields)
57
- index(fields.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
59
+ def primary_key_index(*columns)
60
+ index(columns.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
58
61
  end
59
62
 
60
- def constraint(foreign_key, **options)
61
- constraint_specs << DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_key.to_s, **options)
63
+ 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(
65
+ foreign_key_column.to_s,
66
+ constraint_name: constraint_name,
67
+ child_table_name: table_name, parent_table_name: parent_table_name, parent_class_name: parent_class_name, dependent: dependent
68
+ )
62
69
  end
63
70
 
64
71
  # tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes
@@ -76,7 +83,7 @@ module DeclareSchema
76
83
  _add_serialize_for_field(name, type, options)
77
84
  _add_formatting_for_field(name, type)
78
85
  _add_validations_for_field(name, type, args, options)
79
- _add_index_for_field(name, args, options)
86
+ _add_index_for_field(name, args, **options)
80
87
  field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, position: field_specs.size, **options)
81
88
  attr_order << name unless attr_order.include?(name)
82
89
  end
@@ -91,8 +98,8 @@ module DeclareSchema
91
98
 
92
99
  # Extend belongs_to so that it
93
100
  # 1. creates a FieldSpec for the foreign key
94
- # 2. declares an index on the foreign key
95
- # 3. declares a foreign_key constraint
101
+ # 2. declares an index on the foreign key (optional)
102
+ # 3. declares a foreign_key constraint (optional)
96
103
  def belongs_to(name, scope = nil, **options)
97
104
  column_options = {}
98
105
 
@@ -104,54 +111,52 @@ module DeclareSchema
104
111
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
105
112
  if options.has_key?(:limit)
106
113
  options.delete(:limit)
107
- ActiveSupport::Deprecation.warn("belongs_to limit: is deprecated since it is now inferred")
114
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, limit: is deprecated since it is now inferred")
108
115
  end
109
116
 
110
117
  # index: true means create an index on the foreign key
111
118
  # index: false means do not create an index on the foreign key
112
119
  # index: { ... } means create an index on the foreign key with the given options
113
120
  index_value = options.delete(:index)
114
- if index_value != false || options.has_key?(:unique) || options.has_key?(:allow_equivalent)
115
- index_options = {}
121
+ if index_value == false # don't create an index
122
+ options.delete(:unique)
123
+ options.delete(:allow_equivalent)
124
+ else
125
+ index_options = {} # create an index
116
126
  case index_value
117
- when String
118
- Kernel.warn("belongs_to index: 'name' is deprecated; use index: { name: 'name' } instead (in #{name})")
119
- index_options[:name] = index_value
127
+ when String, Symbol
128
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, index: 'name' is deprecated; use index: { name: 'name' } instead (in #{self.name})")
129
+ index_options[:name] = index_value.to_s
120
130
  when true
121
- when false
122
- raise ArgumentError, "belongs_to index: false contradicts others options #{options.inspect} (in #{name})"
123
131
  when nil
124
132
  when Hash
125
133
  index_options = index_value
126
134
  else
127
- raise ArgumentError, "belongs_to index: must be true or false or a Hash; got #{index_value.inspect} (in #{name})"
135
+ raise ArgumentError, "belongs_to #{name.inspect}, index: must be true or false or a Hash; got #{index_value.inspect} (in #{self.name})"
128
136
  end
129
137
 
130
138
  if options.has_key?(:unique)
131
- Kernel.warn("belongs_to unique: true|false is deprecated; use index: { unique: true|false } instead (in #{name})")
139
+ ActiveSupport::Deprecation.warn("belongs_to #{name.inspect}, unique: true|false is deprecated; use index: { unique: true|false } instead (in #{self.name})")
132
140
  index_options[:unique] = options.delete(:unique)
133
141
  end
134
142
 
135
143
  index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
136
144
  end
137
145
 
138
- fk_options = options.dup
139
- fk_options[:constraint_name] = options.delete(:constraint) if options.has_key?(:constraint)
140
- fk_options[:index_name] = index_options&.[](:name)
146
+ constraint_name = options.delete(:constraint)
141
147
 
142
- fk = options[:foreign_key]&.to_s || "#{name}_id"
148
+ dependent_delete = :delete if options.delete(:far_end_dependent) == :delete
143
149
 
150
+ # infer :optional from :null
144
151
  if !options.has_key?(:optional)
145
- options[:optional] = column_options[:null] # infer :optional from :null
152
+ options[:optional] = column_options[:null]
146
153
  end
147
154
 
148
- fk_options[:dependent] = options.delete(:far_end_dependent) if options.has_key?(:far_end_dependent)
149
-
150
155
  super
151
156
 
152
157
  reflection = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
153
- foreign_key = reflection.foreign_key or raise "Couldn't find foreign_key for #{name} in #{reflection.inspect}"
154
- foreign_key_id_column_options = column_options.dup
158
+ foreign_key_column = reflection.foreign_key or raise "Couldn't find foreign_key for #{name} in #{reflection.inspect}"
159
+ foreign_key_column_options = column_options.dup
155
160
 
156
161
  # Note: the foreign key limit: should match the primary key limit:. (If there is a foreign key constraint,
157
162
  # those limits _must_ match.) We'd like to call _infer_fk_limit and get the limit right from the PK.
@@ -162,27 +167,34 @@ module DeclareSchema
162
167
  # The one downside of this approach is that application code that asks the field_spec for the declared
163
168
  # foreign key limit: will always get 8 back even if this is a grandfathered foreign key that points to
164
169
  # a limit: 4 primary key. It seems unlikely that any application code would do this.
165
- foreign_key_id_column_options[:pre_migration] = ->(field_spec) do
166
- if (inferred_limit = _infer_fk_limit(foreign_key, reflection))
170
+ foreign_key_column_options[:pre_migration] = ->(field_spec) do
171
+ if (inferred_limit = _infer_fk_limit(foreign_key_column, reflection))
167
172
  field_spec.sql_options[:limit] = inferred_limit
168
173
  end
169
174
  end
170
175
 
171
- declare_field(foreign_key.to_sym, :bigint, **foreign_key_id_column_options)
176
+ declare_field(foreign_key_column.to_sym, :bigint, **foreign_key_column_options)
172
177
 
173
178
  if reflection.options[:polymorphic]
174
179
  foreign_type = options[:foreign_type] || "#{name}_type"
175
180
  _declare_polymorphic_type_field(foreign_type, column_options)
176
- index([foreign_type, foreign_key], **index_options) if index_options
181
+ if ::DeclareSchema.default_generate_indexing && index_options
182
+ index([foreign_type, foreign_key_column], **index_options)
183
+ end
177
184
  else
178
- index(foreign_key, **index_options) if index_options
179
- constraint(foreign_key, **fk_options) if fk_options[:constraint_name] != false
185
+ if ::DeclareSchema.default_generate_indexing && index_options
186
+ index([foreign_key_column], **index_options)
187
+ end
188
+
189
+ if ::DeclareSchema.default_generate_foreign_keys && constraint_name != false
190
+ constraint(foreign_key_column, constraint_name: constraint_name || index_options&.[](:name), parent_class_name: reflection.class_name, dependent: dependent_delete)
191
+ end
180
192
  end
181
193
  end
182
194
 
183
- def _infer_fk_limit(foreign_key, reflection)
195
+ def _infer_fk_limit(foreign_key_column, reflection)
184
196
  if reflection.options[:polymorphic]
185
- if (foreign_key_column = columns_hash[foreign_key.to_s]) && foreign_key_column.type == :integer
197
+ if (foreign_key_column = columns_hash[foreign_key_column.to_s]) && foreign_key_column.type == :integer
186
198
  foreign_key_column.limit
187
199
  end
188
200
  else
@@ -224,7 +236,7 @@ module DeclareSchema
224
236
  end
225
237
 
226
238
  def _rails_default_primary_key
227
- ::DeclareSchema::Model::IndexDefinition.new(self, [_declared_primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
239
+ ::DeclareSchema::Model::IndexDefinition.new([_declared_primary_key], name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME, table_name: table_name, unique: true)
228
240
  end
229
241
 
230
242
  # Declares the "foo_type" field that accompanies the "foo_id"
@@ -295,15 +307,16 @@ module DeclareSchema
295
307
  end
296
308
  end
297
309
 
298
- def _add_index_for_field(name, args, options)
299
- if (to_name = options.delete(:index))
310
+ def _add_index_for_field(column_name, args, **options)
311
+ if (index_name = options.delete(:index))
300
312
  index_opts =
301
313
  {
302
314
  unique: args.include?(:unique) || !!options.delete(:unique)
303
315
  }
316
+
304
317
  # support index: true declaration
305
- index_opts[:name] = to_name unless to_name == true
306
- index(name, **index_opts)
318
+ index_opts[:name] = index_name unless index_name == true
319
+ index([column_name], **index_opts)
307
320
  end
308
321
  end
309
322