declare_schema 1.3.3 → 1.3.4.colin.1

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: 001426fc94493c347f28b363268e9dc192f12f87060db6af5f30c71eb94f0207
4
- data.tar.gz: b5a611da740dec158aea306327533df561bc7a7851ad2586c016fbfaeeba4c64
3
+ metadata.gz: 67383598f5b2515ce62994cc33931e14b5f7736f84f142cdba28da87a3765698
4
+ data.tar.gz: 1f11e932b6222419aac070f17878108a05072080469195a5b299bdfe604c26c8
5
5
  SHA512:
6
- metadata.gz: 41376d3bb2313f9939107bd5d111b2628619d3b93784733dd0b51aaf37cbe92cd53e392110830590109e35a1c9385d04d1db7026b11c454604cf483b2bcdb77a
7
- data.tar.gz: a68fcdaa6132bf1e055c17f7cf805e686fe75544bbd10720d7fd2cea662aac26b2b5bca5892c7e13efa0e5392ee361329666f95be1b6d93ef61356d6ba385210
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] - 2023-01-17
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)
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"
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 =
@@ -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
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