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

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: 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