declare_schema 1.3.3.colin.1 → 1.3.4.colin.1

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: 2cc1b63a3d66a299428ec32493d79aa3ef2a3953a663c66c661887f28dffe96e
4
- data.tar.gz: 6712668fe9c41feb156cd0127ceacf57802bbb17c633da408038a0f5b8067509
3
+ metadata.gz: 67383598f5b2515ce62994cc33931e14b5f7736f84f142cdba28da87a3765698
4
+ data.tar.gz: 1f11e932b6222419aac070f17878108a05072080469195a5b299bdfe604c26c8
5
5
  SHA512:
6
- metadata.gz: ad29f584d6b7bd42b6e5b547578b8a0004a5602376e015117b811e726c8d62d2b69bb97a1af32af1635788e6b18a9cfbbc89e185ee496fac3e35567246c9185d
7
- data.tar.gz: 14fc6510af10b9158919f23d8125f9d15cd2c4b34b5cb81ee990332fa924a82f52ca41e08c514be2157539900a10ea19238f0be49ec9fe8ec9c5fb404b0e2b3d
6
+ metadata.gz: e7547cfc57e2c5a06ba8c45923a8aa6bd4631637757a081868081e9d0b63641c9ec24d07eb9bb0eab43e136638e95f4058b01a3b8fa18ee1159d42c6e67f40d9
7
+ data.tar.gz: a09ca6210c25cb97171622d6c04e19213a8c9b8afc3c131926697533ffbc92dff7290aa61025373aff2367846a8bca2394efd20a77fe2e63e1c27675794b9d77
data/CHANGELOG.md CHANGED
@@ -4,7 +4,12 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
5
  Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.3.3] - Unreleased
7
+ ## [1.3.4] - 2024-01-18
8
+ ### Fixed
9
+ - Add test for migrating `has_and_belongs_to_many` associations and fix them to properly declare their
10
+ 2 foreign keys as the primary key of the join table, rather than just a unique index.
11
+
12
+ ## [1.3.3] - 2024-01-17
8
13
  ### Fixed
9
14
  - Fix a MySQL 8 bug where MySQL 8+ renames charset 'utf8' to 'utf8mb3' and collation 'utf8_general_ci' to
10
15
  'utf8mb3_unicode_ci'.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (1.3.3.colin.1)
4
+ declare_schema (1.3.4.colin.1)
5
5
  rails (>= 5.0)
6
6
 
7
7
  GEM
@@ -7,46 +7,53 @@ module DeclareSchema
7
7
  class ForeignKeyDefinition
8
8
  include Comparable
9
9
 
10
- attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :parent_table_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
-
22
- @constraint_name = options[:constraint_name]&.to_s.presence ||
23
- model.connection.index_name(model.table_name, column: @foreign_key_name)
24
- @on_delete_cascade = options[:dependent] == :delete
10
+ attr_reader :foreign_key_column, :constraint_name, :child_table_name, :parent_table_name, :dependent
11
+
12
+ # Caller needs to pass either constraint_name or child_table. The child_table is remembered, but it is not part of the key;
13
+ # it is just used to compute the default constraint_name if no constraint_name is given.
14
+ def initialize(foreign_key_column, constraint_name: nil, child_table: nil, parent_table: nil, class_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, [@foreign_key_column])
17
+ @child_table_name = child_table&.to_s or raise ArgumentError, "child_table must not be nil"
18
+ @parent_table_name = parent_table&.to_s || infer_parent_table_name_from_class(class_name) || infer_parent_table_name_from_foreign_key_column(@foreign_key_column)
19
+ dependent.in?([nil, :delete]) or raise ArgumentError, "dependent: must be nil or :delete"
20
+ @dependent = dependent
25
21
  end
26
22
 
27
23
  class << self
28
- def for_model(model, old_table_name)
29
- show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
24
+ def for_table(table_name, connection, dependent: nil)
25
+ show_create_table = connection.select_rows("show create table #{connection.quote_table_name(table_name)}").first.last
30
26
  constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
31
27
 
32
28
  constraints.map do |fkc|
33
- name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
34
- options = {
35
- constraint_name: name,
36
- parent_table: parent_table,
37
- foreign_key: foreign_key
38
- }
39
- options[:dependent] = :delete if fkc['ON DELETE CASCADE'] || model.is_a?(DeclareSchema::Model::HabtmModelShim)
40
-
41
- new(model, foreign_key, **options)
29
+ constraint_name, foreign_key_column, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
30
+ dependent_value = :delete if dependent || fkc['ON DELETE CASCADE']
31
+
32
+ new(foreign_key_column, constraint_name: constraint_name, child_table: table_name, parent_table: parent_table, dependent: dependent_value)
42
33
  end
43
34
  end
44
35
  end
45
36
 
37
+ def key
38
+ @key ||= [@parent_table_name, @foreign_key_column, @dependent].freeze
39
+ end
40
+
41
+ def <=>(rhs)
42
+ key <=> rhs.key
43
+ end
44
+
45
+ alias eql? ==
46
+
47
+ def equivalent?(rhs)
48
+ self == rhs
49
+ end
50
+
51
+ private
52
+
46
53
  # returns the parent class as a Class object
47
- # or nil if no :class_name option given
48
- def parent_class
49
- if (class_name = options[:class_name])
54
+ # or nil if no @class_name option given
55
+ def parent_class(class_name)
56
+ if class_name
50
57
  if class_name.is_a?(Class)
51
58
  class_name
52
59
  else
@@ -55,22 +62,12 @@ module DeclareSchema
55
62
  end
56
63
  end
57
64
 
58
- def parent_table_name
59
- @parent_table_name ||=
60
- parent_class&.try(:table_name) ||
61
- foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
62
- end
63
-
64
- def <=>(rhs)
65
- key <=> rhs.send(:key)
65
+ def infer_parent_table_name_from_class(class_name)
66
+ parent_class(class_name)&.try(:table_name)
66
67
  end
67
68
 
68
- alias eql? ==
69
-
70
- private
71
-
72
- def key
73
- @key ||= [@child_table_name, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
69
+ def infer_parent_table_name_from_foreign_key_column(foreign_key_column)
70
+ foreign_key_column.sub(/_id\z/, '').camelize.constantize.table_name
74
71
  end
75
72
 
76
73
  def hash
@@ -5,28 +5,21 @@ module DeclareSchema
5
5
  class HabtmModelShim
6
6
  class << self
7
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)
8
+ new(refl.join_table, [refl.foreign_key, refl.association_foreign_key],
9
+ [refl.active_record.table_name, refl.class_name.constantize.table_name])
20
10
  end
21
11
  end
22
12
 
23
- attr_reader :join_table, :foreign_keys, :foreign_key_classes, :connection
13
+ attr_reader :join_table, :foreign_keys, :parent_table_names
24
14
 
25
- def initialize(join_table, foreign_keys, foreign_key_classes, connection)
15
+ def initialize(join_table, foreign_keys, parent_table_names)
16
+ foreign_keys.is_a?(Array) && foreign_keys.size == 2 or
17
+ raise ArgumentError, "foreign_keys must be <Array[2]>; got #{foreign_keys.inspect}"
18
+ parent_table_names.is_a?(Array) && parent_table_names.size == 2 or
19
+ raise ArgumentError, "parent_table_names must be <Array[2]>; got #{parent_table_names.inspect}"
26
20
  @join_table = join_table
27
- @foreign_keys = foreign_keys
28
- @foreign_key_classes = foreign_key_classes
29
- @connection = connection
21
+ @foreign_keys = foreign_keys.sort # Rails requires these be in alphabetical order
22
+ @parent_table_names = @foreign_keys == foreign_keys ? parent_table_names : parent_table_names.reverse # match the above sort
30
23
  end
31
24
 
32
25
  def _table_options
@@ -38,8 +31,8 @@ module DeclareSchema
38
31
  end
39
32
 
40
33
  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)
34
+ foreign_keys.each_with_index.each_with_object({}) do |(foreign_key, i), result|
35
+ result[foreign_key] = ::DeclareSchema::Model::FieldSpec.new(self, foreign_key, :bigint, position: i, null: false)
43
36
  end
44
37
  end
45
38
 
@@ -48,17 +41,21 @@ module DeclareSchema
48
41
  end
49
42
 
50
43
  def _declared_primary_key
51
- false # no single-column primary key declared
44
+ foreign_keys
52
45
  end
53
46
 
54
- def index_definitions_with_primary_key
47
+ def index_definitions
55
48
  [
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
49
+ IndexDefinition.new(foreign_keys.last, table_name: table_name, unique: false) # index for queries where we only have the last foreign key
58
50
  ]
59
51
  end
60
52
 
61
- alias_method :index_definitions, :index_definitions_with_primary_key
53
+ def index_definitions_with_primary_key
54
+ [
55
+ *index_definitions,
56
+ IndexDefinition.new(foreign_keys, name: Model::IndexDefinition::PRIMARY_KEY_NAME, unique: true) # creates a primary composite key on both foreign keys
57
+ ]
58
+ end
62
59
 
63
60
  def ignore_indexes
64
61
  []
@@ -66,8 +63,8 @@ module DeclareSchema
66
63
 
67
64
  def constraint_specs
68
65
  [
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)
66
+ ForeignKeyDefinition.new(foreign_keys.first, child_table: @join_table, parent_table: parent_table_names.first, constraint_name: "#{join_table}_FK1", dependent: :delete),
67
+ ForeignKeyDefinition.new(foreign_keys.last, child_table: @join_table, parent_table: parent_table_names.last, constraint_name: "#{join_table}_FK2", dependent: :delete)
71
68
  ]
72
69
  end
73
70
  end
@@ -7,27 +7,30 @@ module DeclareSchema
7
7
  class IndexDefinition
8
8
  include Comparable
9
9
 
10
- # TODO: replace `fields` with `columns` and remove alias. -Colin
11
- attr_reader :table, :fields, :explicit_name, :name, :unique, :where
12
- alias columns fields
10
+ attr_reader :columns, :explicit_name, :name, :unique, :where
11
+ alias fields columns # TODO: change callers to use columns. -Colin
13
12
 
14
13
  class IndexNameTooLongError < RuntimeError; end
15
14
 
16
15
  PRIMARY_KEY_NAME = "PRIMARY"
17
16
 
18
- def initialize(model, fields, **options)
19
- @model = model
20
- @table = options.delete(:table_name) || model.table_name
21
- @fields = Array.wrap(fields).map(&:to_s)
22
- @explicit_name = options[:name] unless options.delete(:allow_equivalent)
23
- @name = options.delete(:name) || self.class.default_index_name(@table, @fields)
24
- @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
17
+ # Caller needs to pass either name or table_name. The table_name is not remembered; it is just used to compute the
18
+ # default name if no name is given.
19
+ def initialize(columns, name: nil, table_name: nil, allow_equivalent: false, unique: false, where: nil)
20
+ @name = name || self.class.default_index_name(table_name, columns)
21
+ @columns = Array.wrap(columns).map(&:to_s)
22
+ @explicit_name = @name unless allow_equivalent
23
+ unique.in?([false, true]) or raise ArgumentError, "unique must be true or false: got #{unique.inspect}"
24
+ if @name == PRIMARY_KEY_NAME
25
+ unique or raise ArgumentError, "primary key index must be unique"
26
+ end
27
+ @unique = unique
25
28
 
26
29
  if DeclareSchema.max_index_and_constraint_name_length && @name.length > DeclareSchema.max_index_and_constraint_name_length
27
30
  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."
28
31
  end
29
32
 
30
- if (where = options[:where])
33
+ if where
31
34
  @where = where.start_with?('(') ? where : "(#{where})"
32
35
  end
33
36
  end
@@ -35,33 +38,32 @@ module DeclareSchema
35
38
  class << self
36
39
  # extract IndexSpecs from an existing table
37
40
  # includes the PRIMARY KEY index
38
- def for_model(model, old_table_name = nil)
39
- t = old_table_name || model.table_name
40
-
41
- primary_key_columns = Array(model.connection.primary_key(t)).presence
42
- primary_key_columns or raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
41
+ def for_table(table_name, ignore_indexes, connection)
42
+ primary_key_columns = Array(connection.primary_key(table_name))
43
+ primary_key_columns.present? or raise "could not find primary key for table #{table_name} in #{connection.columns(table_name).inspect}"
43
44
 
44
45
  primary_key_found = false
45
- index_definitions = model.connection.indexes(t).map do |i|
46
- model.ignore_indexes.include?(i.name) and next
47
- if i.name == PRIMARY_KEY_NAME
48
- i.columns == primary_key_columns && i.unique or
49
- raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
46
+ index_definitions = connection.indexes(table_name).map do |index|
47
+ next if ignore_indexes.include?(index.name)
48
+
49
+ if index.name == PRIMARY_KEY_NAME
50
+ index.columns == primary_key_columns && index.unique or
51
+ raise "primary key on #{table_name} was not unique on #{primary_key_columns} (was unique=#{index.unique} on #{index.columns})"
50
52
  primary_key_found = true
51
53
  end
52
- new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
54
+ new(index.columns, name: index.name, unique: index.unique, where: index.where)
53
55
  end.compact
54
56
 
55
57
  if !primary_key_found
56
- index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
58
+ index_definitions << new(primary_key_columns, name: PRIMARY_KEY_NAME, unique: true)
57
59
  end
58
60
  index_definitions
59
61
  end
60
62
 
61
- def default_index_name(table, fields)
63
+ def default_index_name(table_name, columns)
62
64
  index_name = nil
63
65
  [:long_index_name, :short_index_name].find do |method_name|
64
- index_name = send(method_name, table, fields)
66
+ index_name = send(method_name, table_name, columns)
65
67
  if DeclareSchema.max_index_and_constraint_name_length.nil? || index_name.length <= DeclareSchema.max_index_and_constraint_name_length
66
68
  break index_name
67
69
  end
@@ -103,11 +105,11 @@ module DeclareSchema
103
105
  end
104
106
 
105
107
  def to_key
106
- @key ||= [table, fields, name, unique, where].map(&:to_s)
108
+ @to_key ||= [name, *settings].freeze
107
109
  end
108
110
 
109
111
  def settings
110
- @settings ||= [table, fields, unique].map(&:to_s)
112
+ @settings ||= [columns, unique, where].freeze
111
113
  end
112
114
 
113
115
  def hash
@@ -123,7 +125,7 @@ module DeclareSchema
123
125
  end
124
126
 
125
127
  def with_name(new_name)
126
- self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
128
+ self.class.new(@columns, name: new_name, unique: @unique, allow_equivalent: @explicit_name.nil?, where: @where)
127
129
  end
128
130
 
129
131
  alias eql? ==
@@ -49,22 +49,34 @@ module DeclareSchema
49
49
  end
50
50
 
51
51
  module ClassMethods
52
- def index(fields, **options)
53
- # make index idempotent
54
- index_fields_s = Array.wrap(fields).map(&:to_s)
55
- unless index_definitions.any? { |index_spec| index_spec.fields == index_fields_s }
56
- index_definitions << ::DeclareSchema::Model::IndexDefinition.new(self, fields, **options)
52
+ def index(columns, name: nil, allow_equivalent: false, unique: false, where: nil)
53
+ index_definition =
54
+ ::DeclareSchema::Model::IndexDefinition.new(
55
+ columns,
56
+ name: name, table_name: table_name, allow_equivalent: allow_equivalent, unique: unique, where: where
57
+ )
58
+
59
+ # add idempotently
60
+ unless index_definitions.any? { |index_def| index_def.equivalent?(index_definition) }
61
+ index_definitions << index_definition
57
62
  end
58
63
  end
59
64
 
60
- def primary_key_index(*fields)
61
- index(fields.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
65
+ def primary_key_index(*columns)
66
+ index(columns.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
62
67
  end
63
68
 
64
- def constraint(fkey, **options)
65
- fkey_s = fkey.to_s
66
- unless constraint_specs.any? { |constraint_spec| constraint_spec.foreign_key == fkey_s }
67
- constraint_specs << DeclareSchema::Model::ForeignKeyDefinition.new(self, fkey, **options)
69
+ def constraint(foreign_key_column, parent_table: nil, constraint_name: nil, class_name: nil, dependent: nil)
70
+ foreign_key_column = foreign_key_column.to_s
71
+ constraint_definition =
72
+ ::DeclareSchema::Model::ForeignKeyDefinition.new(
73
+ foreign_key_column,
74
+ constraint_name: constraint_name,
75
+ child_table: table_name, parent_table: parent_table, class_name: class_name, dependent: dependent
76
+ )
77
+
78
+ unless constraint_specs.any? { |constraint_def| constraint_def.equivalent?(constraint_definition) }
79
+ constraint_specs << constraint_definition
68
80
  end
69
81
  end
70
82
 
@@ -98,8 +110,8 @@ module DeclareSchema
98
110
 
99
111
  # Extend belongs_to so that it
100
112
  # 1. creates a FieldSpec for the foreign key
101
- # 2. declares an index on the foreign key
102
- # 3. declares a foreign_key constraint
113
+ # 2. declares an index on the foreign key (optional)
114
+ # 3. declares a foreign_key constraint (optional)
103
115
  def belongs_to(name, scope = nil, **options)
104
116
  column_options = {}
105
117
 
@@ -119,23 +131,21 @@ module DeclareSchema
119
131
  index_options[:unique] = options.delete(:unique) if options.has_key?(:unique)
120
132
  index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
121
133
 
122
- fk_options = options.dup
123
- fk_options[:constraint_name] = options.delete(:constraint) if options.has_key?(:constraint)
124
- fk_options[:index_name] = index_options[:name]
134
+ constraint_name = options.delete(:constraint)
135
+ index_name = index_options[:name]
125
136
 
126
- fk = options[:foreign_key]&.to_s || "#{name}_id"
137
+ dependent_delete = :delete if options.delete(:far_end_dependent) == :delete
127
138
 
139
+ # infer :optional from :null
128
140
  if !options.has_key?(:optional)
129
- options[:optional] = column_options[:null] # infer :optional from :null
141
+ options[:optional] = column_options[:null]
130
142
  end
131
143
 
132
- fk_options[:dependent] = options.delete(:far_end_dependent) if options.has_key?(:far_end_dependent)
133
-
134
144
  super
135
145
 
136
146
  refl = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
137
- fkey = refl.foreign_key or raise "Couldn't find foreign_key for #{name} in #{refl.inspect}"
138
- fkey_id_column_options = column_options.dup
147
+ foreign_key_column = refl.foreign_key or raise "Couldn't find foreign_key for #{name} in #{refl.inspect}"
148
+ foreign_key_column_options = column_options.dup
139
149
 
140
150
  # Note: the foreign key limit: should match the primary key limit:. (If there is a foreign key constraint,
141
151
  # those limits _must_ match.) We'd like to call _infer_fk_limit and get the limit right from the PK.
@@ -146,27 +156,34 @@ module DeclareSchema
146
156
  # The one downside of this approach is that application code that asks the field_spec for the declared
147
157
  # foreign key limit: will always get 8 back even if this is a grandfathered foreign key that points to
148
158
  # a limit: 4 primary key. It seems unlikely that any application code would do this.
149
- fkey_id_column_options[:pre_migration] = ->(field_spec) do
150
- if (inferred_limit = _infer_fk_limit(fkey, refl))
159
+ foreign_key_column_options[:pre_migration] = ->(field_spec) do
160
+ if (inferred_limit = _infer_fk_limit(foreign_key_column, refl))
151
161
  field_spec.sql_options[:limit] = inferred_limit
152
162
  end
153
163
  end
154
164
 
155
- declare_field(fkey.to_sym, :bigint, **fkey_id_column_options)
165
+ declare_field(foreign_key_column.to_sym, :bigint, **foreign_key_column_options)
156
166
 
157
167
  if refl.options[:polymorphic]
158
168
  foreign_type = options[:foreign_type] || "#{name}_type"
159
169
  _declare_polymorphic_type_field(foreign_type, column_options)
160
- index([foreign_type, fkey], **index_options) if index_options[:name] != false
170
+ if ::DeclareSchema.default_generate_indexing && index_name != false
171
+ index([foreign_type, foreign_key_column], **index_options)
172
+ end
161
173
  else
162
- index(fkey, **index_options) if index_options[:name] != false
163
- constraint(fkey, **fk_options) if fk_options[:constraint_name] != false
174
+ if ::DeclareSchema.default_generate_indexing && index_name != false
175
+ index(foreign_key_column, **index_options)
176
+ end
177
+
178
+ if ::DeclareSchema.default_generate_foreign_keys && constraint_name != false
179
+ constraint(foreign_key_column, parent_table: nil, constraint_name: constraint_name || index_name, class_name: refl.klass, dependent: dependent_delete)
180
+ end
164
181
  end
165
182
  end
166
183
 
167
- def _infer_fk_limit(fkey, refl)
184
+ def _infer_fk_limit(foreign_key_column, refl)
168
185
  if refl.options[:polymorphic]
169
- if (fkey_column = columns_hash[fkey.to_s]) && fkey_column.type == :integer
186
+ if (fkey_column = columns_hash[foreign_key_column.to_s]) && fkey_column.type == :integer
170
187
  fkey_column.limit
171
188
  end
172
189
  else
@@ -208,7 +225,7 @@ module DeclareSchema
208
225
  end
209
226
 
210
227
  def _rails_default_primary_key
211
- ::DeclareSchema::Model::IndexDefinition.new(self, [_declared_primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
228
+ ::DeclareSchema::Model::IndexDefinition.new([_declared_primary_key], name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME, unique: true)
212
229
  end
213
230
 
214
231
  # Declares the "foo_type" field that accompanies the "foo_id"
@@ -279,15 +296,14 @@ module DeclareSchema
279
296
  end
280
297
  end
281
298
 
282
- def _add_index_for_field(name, args, options)
283
- if (to_name = options.delete(:index))
284
- index_opts =
285
- {
286
- unique: args.include?(:unique) || options.delete(:unique)
287
- }
288
- # support index: true declaration
289
- index_opts[:name] = to_name unless to_name == true
290
- index(name, **index_opts)
299
+ def _add_index_for_field(column_name, args, options)
300
+ if (index_name = options.delete(:index))
301
+ if index_name == true
302
+ index_name = nil # index: true means generate default name
303
+ end
304
+ unique = args.include?(:unique) || options.delete(:unique) || false
305
+
306
+ index([column_name], unique: unique, name: index_name)
291
307
  end
292
308
  end
293
309
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "1.3.3.colin.1"
4
+ VERSION = "1.3.4.colin.1"
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_record'
4
4
  require 'active_record/connection_adapters/abstract_adapter'
5
5
  require 'declare_schema/schema_change/all'
6
+ require_relative '../../../declare_schema/model/habtm_model_shim'
6
7
 
7
8
  module Generators
8
9
  module DeclareSchema
@@ -221,8 +222,8 @@ module Generators
221
222
  ::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
222
223
  end
223
224
 
224
- creates = to_create.map do |t|
225
- model = models_by_table_name[t]
225
+ creates = to_create.map do |table_name|
226
+ model = models_by_table_name[table_name]
226
227
  disable_auto_increment = model.try(:disable_auto_increment)
227
228
 
228
229
  primary_key_definition =
@@ -239,10 +240,13 @@ module Generators
239
240
  table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, **table_options_for_model(model))
240
241
  table_options = create_table_options(model, disable_auto_increment)
241
242
 
242
- table_add = ::DeclareSchema::SchemaChange::TableAdd.new(t,
243
- primary_key_definition + field_definitions,
244
- table_options,
245
- sql_options: table_options_definition.settings)
243
+ table_add = ::DeclareSchema::SchemaChange::TableAdd.new(
244
+ table_name,
245
+ primary_key_definition + field_definitions,
246
+ table_options,
247
+ sql_options: table_options_definition.settings
248
+ )
249
+
246
250
  [
247
251
  table_add,
248
252
  *Array((create_indexes(model) if ::DeclareSchema.default_generate_indexing)),
@@ -255,9 +259,9 @@ module Generators
255
259
  fk_changes = []
256
260
  table_options_changes = []
257
261
 
258
- to_change.each do |t|
259
- model = models_by_table_name[t]
260
- table = to_rename.key(t) || model.table_name
262
+ to_change.each do |table_name|
263
+ model = models_by_table_name[table_name]
264
+ table = to_rename.key(table_name) || model.table_name
261
265
  if table.in?(db_tables)
262
266
  change, index_change, fk_change, table_options_change = change_table(model, table)
263
267
  changes << change
@@ -311,6 +315,8 @@ module Generators
311
315
  { id: false }
312
316
  elsif primary_key == "id"
313
317
  { id: :bigint }
318
+ elsif primary_key.is_a?(Array)
319
+ { primary_key: primary_key.map(&:to_sym) }
314
320
  else
315
321
  { primary_key: primary_key.to_sym }
316
322
  end.merge(model._table_options)
@@ -337,7 +343,7 @@ module Generators
337
343
  def create_constraints(model)
338
344
  model.constraint_specs.map do |fk|
339
345
  ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
340
- column_name: fk.foreign_key_name, name: fk.constraint_name)
346
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
341
347
  end
342
348
  end
343
349
 
@@ -425,7 +431,7 @@ module Generators
425
431
  ::DeclareSchema.default_generate_indexing or return []
426
432
 
427
433
  new_table_name = model.table_name
428
- existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
434
+ existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_table(old_table_name || new_table_name, model.ignore_indexes, model.connection)
429
435
  model_indexes_with_equivalents = model.index_definitions_with_primary_key
430
436
  model_indexes = model_indexes_with_equivalents.map do |i|
431
437
  if i.explicit_name.nil?
@@ -485,7 +491,11 @@ module Generators
485
491
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
486
492
  ::DeclareSchema.default_generate_foreign_keys or return []
487
493
 
488
- existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
494
+ if model.is_a?(::DeclareSchema::Model::HabtmModelShim)
495
+ force_dependent_delete = :delete
496
+ end
497
+
498
+ existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_table(old_table_name || model.table_name, model.connection, dependent: force_dependent_delete)
489
499
  model_fks = model.constraint_specs
490
500
 
491
501
  fks_to_drop = existing_fks - model_fks
@@ -495,13 +505,13 @@ module Generators
495
505
 
496
506
  drop_fks = (fks_to_drop - renamed_fks_to_drop).map do |fk|
497
507
  ::DeclareSchema::SchemaChange::ForeignKeyRemove.new(fk.child_table_name, fk.parent_table_name,
498
- column_name: fk.foreign_key_name, name: fk.constraint_name)
508
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
499
509
  end
500
510
 
501
511
  add_fks = (fks_to_add - renamed_fks_to_add).map do |fk|
502
512
  # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
503
513
  ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
504
- column_name: fk.foreign_key_name, name: fk.constraint_name)
514
+ column_name: fk.foreign_key_column, name: fk.constraint_name)
505
515
  end
506
516
 
507
517
  [drop_fks + add_fks]
@@ -523,7 +533,7 @@ module Generators
523
533
  end
524
534
 
525
535
  def fk_field_options(model, field_name)
526
- foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }
536
+ foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key_column }
527
537
  if foreign_key && (parent_table = foreign_key.parent_table_name)
528
538
  parent_columns = connection.columns(parent_table) rescue []
529
539
  pk_limit =
@@ -66,7 +66,7 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
66
66
  if defined?(Mysql2)
67
67
  context 'when running on MySQL 8.0' do
68
68
  around do |spec|
69
- DeclareSchema.mysql_version = '8.0.21'
69
+ DeclareSchema.mysql_version = Gem::Version.new('8.0.21')
70
70
  spec.run
71
71
  ensure
72
72
  DeclareSchema.remove_instance_variable('@mysql_version') rescue nil
@@ -809,6 +809,8 @@ RSpec.describe 'DeclareSchema Migration Generator' do
809
809
  expect(User.field_specs.keys).to eq(['company'])
810
810
  expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
811
811
 
812
+ nuke_model_class(User)
813
+
812
814
  ## validates
813
815
 
814
816
  # DeclareSchema can accept a validates hash in the field options.
@@ -829,6 +831,44 @@ RSpec.describe 'DeclareSchema Migration Generator' do
829
831
  up, _down = Generators::DeclareSchema::Migration::Migrator.run
830
832
  ActiveRecord::Migration.class_eval(up)
831
833
  expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
834
+
835
+ # DeclareSchema supports has_and_belongs_to_many relationships and generates the intersection ("join") table
836
+ # with appropriate primary key, indexes, and foreign keys.
837
+
838
+ class Advertiser < ActiveRecord::Base
839
+ declare_schema do
840
+ string :name, limit: 250
841
+ end
842
+ has_and_belongs_to_many :creatives
843
+ end
844
+ class Creative < ActiveRecord::Base
845
+ declare_schema do
846
+ string :url, limit: 500
847
+ end
848
+ has_and_belongs_to_many :advertisers
849
+ end
850
+
851
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to(
852
+ migrate_up(<<~EOS.strip)
853
+ create_table :advertisers, id: :bigint#{create_table_charset_and_collation} do |t|
854
+ t.string :name, limit: 250, null: false#{charset_and_collation}
855
+ end
856
+ create_table :advertisers_creatives, primary_key: [:advertiser_id, :creative_id]#{create_table_charset_and_collation} do |t|
857
+ t.integer :advertiser_id, limit: 8, null: false
858
+ t.integer :creative_id, limit: 8, null: false
859
+ end
860
+ create_table :creatives, id: :bigint#{create_table_charset_and_collation} do |t|
861
+ t.string :url, limit: 500, null: false#{charset_and_collation}
862
+ end
863
+ add_index :advertisers_creatives, [:creative_id], name: :index_advertisers_creatives_on_creative_id
864
+ add_foreign_key :advertisers_creatives, :advertisers, column: :advertiser_id, name: :advertisers_creatives_FK1
865
+ add_foreign_key :advertisers_creatives, :creatives, column: :creative_id, name: :advertisers_creatives_FK2
866
+ EOS
867
+ )
868
+
869
+ nuke_model_class(Ad)
870
+ nuke_model_class(Advertiser)
871
+ nuke_model_class(Creative)
832
872
  end
833
873
 
834
874
  context 'models with the same parent foreign key relation' do
@@ -852,7 +892,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
852
892
  end
853
893
  end
854
894
 
855
- it 'will genereate unique constraint names' do
895
+ it 'will generate unique constraint names' do
856
896
  expect(Generators::DeclareSchema::Migration::Migrator.run).to(
857
897
  migrate_up(<<~EOS.strip)
858
898
  create_table :categories, id: :bigint, options: "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin" do |t|
@@ -21,9 +21,9 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
21
21
  describe 'instance methods' do
22
22
  let(:connection) { instance_double(ActiveRecord::Base.connection.class) }
23
23
  let(:model) { instance_double('Model', table_name: 'models', connection: connection) }
24
- let(:foreign_key) { :network_id }
25
- let(:options) { {} }
26
- subject { described_class.new(model, foreign_key, **options)}
24
+ let(:foreign_key_column) { :network_id }
25
+ let(:options) { { child_table: 'networks' } }
26
+ subject { described_class.new(foreign_key_column, **options)}
27
27
 
28
28
  before do
29
29
  allow(model.connection).to receive(:index_name).with(any_args) { 'index_on_network_id' }
@@ -31,45 +31,44 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
31
31
 
32
32
  describe '#initialize' do
33
33
  it 'normalizes symbols to strings' do
34
- expect(subject.foreign_key).to eq('network_id')
34
+ expect(subject.foreign_key_column).to eq('network_id')
35
35
  expect(subject.parent_table_name).to eq('networks')
36
36
  end
37
37
 
38
38
  context 'when most options passed' do
39
- let(:options) { { parent_table: :networks, foreign_key: :the_network_id } }
39
+ let(:options) { { child_table: 'networks', parent_table: :networks } }
40
40
 
41
41
  it 'normalizes symbols to strings' do
42
- expect(subject.foreign_key).to eq('network_id')
43
- expect(subject.foreign_key_name).to eq('the_network_id')
42
+ expect(subject.foreign_key_column).to eq('network_id')
44
43
  expect(subject.parent_table_name).to eq('networks')
45
- expect(subject.foreign_key).to eq('network_id')
46
- expect(subject.constraint_name).to eq('index_on_network_id')
47
- expect(subject.on_delete_cascade).to be_falsey
44
+ expect(subject.foreign_key_column).to eq('network_id')
45
+ expect(subject.constraint_name).to eq('index_networks_on_network_id')
46
+ expect(subject.dependent).to be_nil
48
47
  end
49
48
  end
50
49
 
51
50
  context 'when all options passed' do
52
- let(:options) { { parent_table: :networks, foreign_key: :the_network_id, constraint_name: :constraint_1, dependent: :delete } }
51
+ let(:options) { { child_table: 'networks', parent_table: :networks, constraint_name: :constraint_1, dependent: :delete } }
53
52
 
54
53
  it 'normalizes symbols to strings' do
55
- expect(subject.foreign_key).to eq('network_id')
56
- expect(subject.foreign_key_name).to eq('the_network_id')
54
+ expect(subject.foreign_key_column).to eq('network_id')
57
55
  expect(subject.parent_table_name).to eq('networks')
58
56
  expect(subject.constraint_name).to eq('constraint_1')
59
- expect(subject.on_delete_cascade).to be_truthy
57
+ expect(subject.dependent).to eq(:delete)
60
58
  end
61
59
  end
62
60
 
63
61
  context 'when constraint name passed as empty string' do
64
- let(:options) { { constraint_name: "" } }
62
+ let(:options) { { child_table: 'networks', constraint_name: "" } }
63
+
65
64
  it 'defaults to rails constraint name' do
66
- expect(subject.constraint_name).to eq("index_on_network_id")
65
+ expect(subject.constraint_name).to eq("index_networks_on_network_id")
67
66
  end
68
67
  end
69
68
 
70
69
  context 'when no constraint name passed' do
71
70
  it 'defaults to rails constraint name' do
72
- expect(subject.constraint_name).to eq("index_on_network_id")
71
+ expect(subject.constraint_name).to eq("index_networks_on_network_id")
73
72
  end
74
73
  end
75
74
  end
@@ -85,13 +84,13 @@ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
85
84
  allow(connection).to receive(:index_name).with('models', column: 'network_id') { }
86
85
  end
87
86
 
88
- describe '.for_model' do
89
- subject { described_class.for_model(model, old_table_name) }
87
+ describe '.for_table' do
88
+ subject { described_class.for_table(old_table_name, model.connection) }
90
89
 
91
- it 'returns new object' do
92
- expect(subject.size).to eq(1), subject.inspect
93
- expect(subject.first).to be_kind_of(described_class)
94
- expect(subject.first.foreign_key).to eq('network_id')
90
+ it 'returns definitions' do
91
+ expect(subject.map(&:key)).to eq([
92
+ ["networks", "network_id", nil]
93
+ ])
95
94
  end
96
95
  end
97
96
  end
@@ -8,19 +8,19 @@ end
8
8
  require_relative '../../../../lib/declare_schema/model/habtm_model_shim'
9
9
 
10
10
  RSpec.describe DeclareSchema::Model::HabtmModelShim do
11
- let(:join_table) { "parent_1_parent_2" }
12
- let(:foreign_keys) { ["parent_1_id", "parent_2_id"] }
13
- let(:foreign_key_classes) { [Parent1, Parent2] }
11
+ let(:join_table) { "customers_users" }
12
+ let(:foreign_keys) { ["user_id", "customer_id"] }
13
+ let(:parent_table_names) { ["users", "customers"] }
14
14
 
15
15
  before do
16
16
  load File.expand_path('../prepare_testapp.rb', __dir__)
17
17
 
18
- class Parent1 < ActiveRecord::Base
19
- self.table_name = "parent_1s"
18
+ class User < ActiveRecord::Base
19
+ self.table_name = "users"
20
20
  end
21
21
 
22
- class Parent2 < ActiveRecord::Base
23
- self.table_name = "parent_2s"
22
+ class Customer < ActiveRecord::Base
23
+ self.table_name = "customers"
24
24
  end
25
25
  end
26
26
 
@@ -29,12 +29,14 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
29
29
  let(:reflection) { double("reflection", join_table: join_table,
30
30
  foreign_key: foreign_keys.first,
31
31
  association_foreign_key: foreign_keys.last,
32
- active_record: foreign_key_classes.first,
33
- class_name: 'Parent1') }
32
+ active_record: User,
33
+ class_name: 'Customer') }
34
34
  it 'returns a new object' do
35
35
  result = described_class.from_reflection(reflection)
36
36
 
37
37
  expect(result).to be_a(described_class)
38
+ expect(result.foreign_keys).to eq(foreign_keys.reverse)
39
+ expect(result.parent_table_names).to eq(parent_table_names.reverse)
38
40
  end
39
41
  end
40
42
  end
@@ -42,14 +44,12 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
42
44
  describe 'instance methods' do
43
45
  let(:connection) { instance_double(ActiveRecord::Base.connection.class, "connection") }
44
46
 
45
- subject { described_class.new(join_table, foreign_keys, foreign_key_classes, connection) }
47
+ subject { described_class.new(join_table, foreign_keys, parent_table_names) }
46
48
 
47
49
  describe '#initialize' do
48
50
  it 'stores initialization attributes' do
49
51
  expect(subject.join_table).to eq(join_table)
50
- expect(subject.foreign_keys).to eq(foreign_keys)
51
- expect(subject.foreign_key_classes).to be(foreign_key_classes)
52
- expect(subject.connection).to be(connection)
52
+ expect(subject.foreign_keys).to eq(foreign_keys.reverse)
53
53
  end
54
54
  end
55
55
 
@@ -67,51 +67,52 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
67
67
 
68
68
  describe '#field_specs' do
69
69
  it 'returns 2 field specs' do
70
- result = subject.field_specs
71
- expect(result.size).to eq(2), result.inspect
72
-
73
- expect(result[foreign_keys.first]).to be_a(::DeclareSchema::Model::FieldSpec)
74
- expect(result[foreign_keys.first].model).to eq(subject)
75
- expect(result[foreign_keys.first].name.to_s).to eq(foreign_keys.first)
76
- expect(result[foreign_keys.first].type).to eq(:integer)
77
- expect(result[foreign_keys.first].position).to eq(0)
78
-
79
- expect(result[foreign_keys.last]).to be_a(::DeclareSchema::Model::FieldSpec)
80
- expect(result[foreign_keys.last].model).to eq(subject)
81
- expect(result[foreign_keys.last].name.to_s).to eq(foreign_keys.last)
82
- expect(result[foreign_keys.last].type).to eq(:integer)
83
- expect(result[foreign_keys.last].position).to eq(1)
70
+ field_specs = subject.field_specs
71
+ expect(field_specs.size).to eq(2), field_specs.inspect
72
+
73
+ expect(field_specs[foreign_keys.first]).to be_a(::DeclareSchema::Model::FieldSpec)
74
+ expect(field_specs[foreign_keys.first].model).to eq(subject)
75
+ expect(field_specs[foreign_keys.first].name.to_s).to eq(foreign_keys.first)
76
+ expect(field_specs[foreign_keys.first].type).to eq(:integer)
77
+ expect(field_specs[foreign_keys.first].position).to eq(1)
78
+
79
+ expect(field_specs[foreign_keys.last]).to be_a(::DeclareSchema::Model::FieldSpec)
80
+ expect(field_specs[foreign_keys.last].model).to eq(subject)
81
+ expect(field_specs[foreign_keys.last].name.to_s).to eq(foreign_keys.last)
82
+ expect(field_specs[foreign_keys.last].type).to eq(:integer)
83
+ expect(field_specs[foreign_keys.last].position).to eq(0)
84
84
  end
85
85
  end
86
86
 
87
87
  describe '#primary_key' do
88
- it 'returns false' do
89
- expect(subject._declared_primary_key).to eq(false)
88
+ it 'returns false because there is no single-column PK for ActiveRecord to use' do
89
+ expect(subject.primary_key).to eq(false)
90
90
  end
91
91
  end
92
92
 
93
93
  describe '#_declared_primary_key' do
94
- it 'returns false' do
95
- expect(subject._declared_primary_key).to eq(false)
94
+ it 'returns the foreign key pair that are used as the primary key in the database' do
95
+ expect(subject._declared_primary_key).to eq(["customer_id", "user_id"])
96
96
  end
97
97
  end
98
98
 
99
99
  describe '#index_definitions_with_primary_key' do
100
100
  it 'returns 2 index definitions' do
101
- result = subject.index_definitions_with_primary_key
102
- expect(result.size).to eq(2), result.inspect
101
+ index_definitions = subject.index_definitions_with_primary_key
102
+ expect(index_definitions.size).to eq(2), index_definitions.inspect
103
103
 
104
- expect(result.first).to be_a(::DeclareSchema::Model::IndexDefinition)
105
- expect(result.first.name).to eq('PRIMARY')
106
- expect(result.first.fields).to eq(['parent_1_id', 'parent_2_id'])
107
- expect(result.first.unique).to be_truthy
104
+ expect(index_definitions.last).to be_a(::DeclareSchema::Model::IndexDefinition)
105
+ expect(index_definitions.last.name).to eq('PRIMARY')
106
+ expect(index_definitions.last.fields).to eq(foreign_keys.reverse)
107
+ expect(index_definitions.last.unique).to be_truthy
108
108
  end
109
109
  end
110
110
 
111
111
  context 'when table and foreign key names are long' do
112
112
  let(:join_table) { "advertiser_campaigns_tracking_pixels" }
113
- let(:foreign_keys) { ['advertiser_campaign', 'tracking_pixel'] }
114
- let(:foreign_key_classes) { [Table1, Table2] }
113
+ let(:foreign_keys_and_table_names) { [["advertiser_id", "advertisers"], ["campaign_id", "campaigns"]] }
114
+ let(:foreign_keys) { foreign_keys_and_table_names.map(&:first) }
115
+ let(:parent_table_names) { foreign_keys_and_table_names.map(&:last) }
115
116
 
116
117
  before do
117
118
  class Table1 < ActiveRecord::Base
@@ -124,19 +125,40 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
124
125
  end
125
126
 
126
127
  it 'returns two index definitions and does not raise a IndexNameTooLongError' do
127
- result = subject.index_definitions_with_primary_key
128
- expect(result.size).to eq(2), result.inspect
129
- expect(result.first).to be_a(::DeclareSchema::Model::IndexDefinition)
130
- expect(result.first.name).to eq('PRIMARY')
131
- expect(result.first.fields).to eq(['advertiser_campaign', 'tracking_pixel'])
132
- expect(result.first.unique).to be_truthy
128
+ indexes = subject.index_definitions_with_primary_key
129
+ expect(indexes.size).to eq(2), indexes.inspect
130
+ expect(indexes.last).to be_a(::DeclareSchema::Model::IndexDefinition)
131
+ expect(indexes.last.name).to eq('PRIMARY')
132
+ expect(indexes.last.fields).to eq(foreign_keys)
133
+ expect(indexes.last.unique).to be_truthy
134
+ expect(indexes.first).to be_a(::DeclareSchema::Model::IndexDefinition)
135
+ expect(indexes.first.name).to eq('index_advertiser_campaigns_tracking_pixels_on_campaign_id')
136
+ expect(indexes.first.fields).to eq([foreign_keys.last])
137
+ expect(indexes.first.unique).to be_falsey
133
138
  end
134
139
  end
135
140
 
136
141
  describe '#index_definitions' do
142
+ it 'returns index_definitions' do
143
+ indexes = subject.index_definitions
144
+ expect(indexes.size).to eq(1), indexes.inspect
145
+ expect(indexes.first.columns).to eq(["user_id"])
146
+ options = [:name, :unique, :where].map { |k| [k, indexes.first.send(k)] }.to_h
147
+ expect(options).to eq(name: "index_customers_users_on_user_id",
148
+ unique: false,
149
+ where: nil)
150
+ end
151
+ end
152
+
153
+ describe '#index_definitions_with_primary_key' do
137
154
  it 'returns index_definitions_with_primary_key' do
138
- result = subject.index_definitions
139
- expect(result.size).to eq(2), result.inspect
155
+ indexes = subject.index_definitions_with_primary_key
156
+ expect(indexes.size).to eq(2), indexes.inspect
157
+ expect(indexes.last.columns).to eq(["customer_id", "user_id"])
158
+ options = [:name, :unique, :where].map { |k| [k, indexes.last.send(k)] }.to_h
159
+ expect(options).to eq(name: "PRIMARY",
160
+ unique: true,
161
+ where: nil)
140
162
  end
141
163
  end
142
164
 
@@ -148,18 +170,11 @@ RSpec.describe DeclareSchema::Model::HabtmModelShim do
148
170
 
149
171
  describe '#constraint_specs' do
150
172
  it 'returns 2 foreign keys' do
151
- result = subject.constraint_specs
152
- expect(result.size).to eq(2), result.inspect
153
-
154
- expect(result.first).to be_a(::DeclareSchema::Model::ForeignKeyDefinition)
155
- expect(result.first.foreign_key).to eq(foreign_keys.first)
156
- expect(result.first.parent_table_name).to be(Parent1.table_name)
157
- expect(result.first.on_delete_cascade).to be_truthy
158
-
159
- expect(result.last).to be_a(::DeclareSchema::Model::ForeignKeyDefinition)
160
- expect(result.last.foreign_key).to eq(foreign_keys.last)
161
- expect(result.last.parent_table_name).to be(Parent2.table_name)
162
- expect(result.last.on_delete_cascade).to be_truthy
173
+ constraints = subject.constraint_specs
174
+ expect(constraints.map(&:key)).to eq([
175
+ ["customers", "customer_id", :delete],
176
+ ["users", "user_id", :delete]
177
+ ])
163
178
  end
164
179
  end
165
180
  end
@@ -11,6 +11,7 @@ require_relative '../../../../lib/declare_schema/model/index_definition'
11
11
 
12
12
  RSpec.describe DeclareSchema::Model::IndexDefinition do
13
13
  let(:model_class) { IndexDefinitionTestModel }
14
+ let(:table_name) { model_class.table_name }
14
15
 
15
16
  context 'Using declare_schema' do
16
17
  before do
@@ -34,28 +35,27 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
34
35
  end
35
36
  end
36
37
 
37
- describe 'instance methods' do
38
- let(:model) { model_class.new }
39
- subject { declared_class.new(model_class) }
38
+ # TODO: create model_spec.rb and move the Model specs below into it. -Colin
39
+ context 'Model class methods' do
40
+ describe '.has index_definitions' do
41
+ subject { model_class.index_definitions }
40
42
 
41
- it 'has index_definitions' do
42
- expect(model_class.index_definitions).to be_kind_of(Array)
43
- expect(model_class.index_definitions.map(&:name)).to eq(['index_index_definition_test_models_on_name'])
44
- expect([:name, :fields, :unique].map { |attr| model_class.index_definitions[0].send(attr)}).to eq(
45
- ['index_index_definition_test_models_on_name', ['name'], false]
46
- )
43
+ it 'returns indexes without primary key' do
44
+ expect(subject.map(&:to_key)).to eq([
45
+ ['index_index_definition_test_models_on_name', ['name'], false, nil],
46
+ ])
47
+ end
47
48
  end
48
49
 
49
- it 'has index_definitions_with_primary_key' do
50
- expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
51
- result = model_class.index_definitions_with_primary_key.sort_by(&:name)
52
- expect(result.map(&:name)).to eq(['PRIMARY', 'index_index_definition_test_models_on_name'])
53
- expect([:name, :fields, :unique].map { |attr| result[0].send(attr)}).to eq(
54
- ['PRIMARY', ['id'], true]
55
- )
56
- expect([:name, :fields, :unique].map { |attr| result[1].send(attr)}).to eq(
57
- ['index_index_definition_test_models_on_name', ['name'], false]
58
- )
50
+ describe '.has index_definitions_with_primary_key' do
51
+ subject { model_class.index_definitions_with_primary_key }
52
+
53
+ it 'returns indexes with primary key' do
54
+ expect(subject.map(&:to_key)).to eq([
55
+ ['index_index_definition_test_models_on_name', ['name'], false, nil],
56
+ ['PRIMARY', ['id'], true, nil],
57
+ ])
58
+ end
59
59
  end
60
60
  end
61
61
 
@@ -80,29 +80,36 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
80
80
  ActiveRecord::Base.connection.schema_cache.clear!
81
81
  end
82
82
 
83
- describe 'for_model' do
84
- subject { described_class.for_model(model_class) }
83
+ describe 'for_table' do
84
+ let(:ignore_indexes) { model_class.ignore_indexes }
85
+ subject { described_class.for_table(model_class.table_name, ignore_indexes, model_class.connection) }
85
86
 
86
87
  context 'with single-column PK' do
87
88
  it 'returns the indexes for the model' do
88
- expect(subject.size).to eq(2), subject.inspect
89
- expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
90
- ['index_definition_test_models_on_name', ['name'], true]
91
- )
92
- expect([:name, :columns, :unique].map { |attr| subject[1].send(attr) }).to eq(
93
- ['PRIMARY', ['id'], true]
94
- )
89
+ expect(subject.map(&:to_key)).to eq([
90
+ ["index_definition_test_models_on_name", ["name"], true, nil],
91
+ ["PRIMARY", ["id"], true, nil]
92
+ ])
95
93
  end
96
94
  end
97
95
 
98
- context 'with compound-column PK' do
96
+ context 'with composite (multi-column) PK' do
99
97
  let(:model_class) { IndexDefinitionCompoundIndexModel }
100
98
 
101
99
  it 'returns the indexes for the model' do
102
- expect(subject.size).to eq(1), subject.inspect
103
- expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
104
- ['PRIMARY', ['fk1_id', 'fk2_id'], true]
105
- )
100
+ expect(subject.map(&:to_key)).to eq([
101
+ ["PRIMARY", ["fk1_id", "fk2_id"], true, nil]
102
+ ])
103
+ end
104
+ end
105
+
106
+ context 'with ignored_indexes' do
107
+ let(:ignore_indexes) { ['index_definition_test_models_on_name'] }
108
+
109
+ it 'skips the ignored index' do
110
+ expect(subject.map(&:to_key)).to eq([
111
+ ["PRIMARY", ["id"], true, nil]
112
+ ])
106
113
  end
107
114
  end
108
115
  end
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.3.3.colin.1
4
+ version: 1.3.4.colin.1
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-17 00:00:00.000000000 Z
11
+ date: 2024-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails