declare_schema 0.2.0 → 0.4.0

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: 3f3116a6fbb5a47de5809f3f813c88783284bad601452679fa59784f3598834b
4
- data.tar.gz: 8f4b6fee7b4d17e5b644f32a4b624a3f77526f91826a8c0b82d61b6e25130332
3
+ metadata.gz: 1e5c665ec203dd84e445290bac1de190491560ddc409fe2bd3f5ba4831e065c5
4
+ data.tar.gz: 244148b892d2de22a692c4db50b136995f97d8da38573e87a443303fdee5be7a
5
5
  SHA512:
6
- metadata.gz: b833000d28d64e7856de05c9ae822c88dcceeb7861f777f13177ea8017e56bf937aba42b9da14ae1ce4810fd275c5462cb08c18c32839255ab46f1de116eee1d
7
- data.tar.gz: c41c8cbdafd96eeb92fcf5c646ff9d18f4ce2374f06ecc6977015fa8406bc4639ab3722354212d1a75274c5af982f10b6c2f4c838de94f4b9436da20f836d5d3
6
+ metadata.gz: 44994fd571b97768ffec2c9bbce779b8beef222ba50213a425a3021c66f1dc4e4fd7a50adc95cf3d1a1116e67eaf5c47b11e635408856c726118a22c18188b98
7
+ data.tar.gz: 7d3eb3be095a1dce65b6b39b35974a6c96ee940fec31dba3fddbfb528be8be3b42906ec81c6075cde5d8fa590df4afc8ca12887a956c772fc34c084791577526
@@ -4,6 +4,37 @@ 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
+ ## [0.4.0] - 2020-11-20
8
+ ### Added
9
+ - Fields may be declared with `serialize: true` or `serialize: <serializeable-class>`, where `<serializeable-class>`
10
+ may be `Array` (`Array` stored as YAML) or `Hash` (`Hash` stored as YAML), (`Array` or `Hash` or any scalar value stored as JSON)
11
+ or any custom serializable class.
12
+ This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
13
+
14
+ Note: when `serialize:` is used, any `default:` should be given in a matching Ruby type--for example, `[]` or `{}` or `{ 'currency' => 'USD' }`--in
15
+ which case the serializeable class will be used to determine the serialized default value and that will be set as the SQL default.
16
+
17
+ ### Fixed
18
+ - Sqlite now correctly infers the PRIMARY KEY so it won't attempt to add that index again.
19
+
20
+ ## [0.3.1] - 2020-11-13
21
+ ### Fixed
22
+ - When passing `belongs_to` to Rails, suppress the `optional:` option in Rails 4, since that option was added in Rails 5.
23
+
24
+ ## [0.3.0] - 2020-11-02
25
+ ### Added
26
+ - Added support for `belongs_to optional:`.
27
+ If given, it is passed through to `ActiveRecord`'s `belong_to`.
28
+ If not given in Rails 5+, the `optional:` value is set equal to the `null:` value (default: `false`) and that
29
+ is passed to `ActiveRecord`'s `belong_to`.
30
+ Similarly, if `null:` is not given, it is inferred from `optional:`.
31
+ If both are given, their values are respected, even if contradictory;
32
+ this is a legitimate case when migrating to/from an optional association.
33
+ - Added a new callback `before_generating_migration` to the `Migrator` that can be
34
+ defined in order to custom load more models that might be missed by `eager_load!`
35
+ ### Fixed
36
+ - Migrations are now generated where the `[4.2]` is only applied after `ActiveRecord::Migration` in Rails 5+ (since Rails 4 didn't know about that notation).
37
+
7
38
  ## [0.2.0] - 2020-10-26
8
39
  ### Added
9
40
  - Automatically eager_load! all Rails::Engines before generating migrations.
@@ -13,7 +44,7 @@ Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0
13
44
 
14
45
  ### Fixed
15
46
  - Fixed a bug where `:text limit: 0xffff_ffff` (max size) was omitted from migrations.
16
- - Fixed a bug where `:bigint` foreign keys were omitted from the migration.
47
+ - Fixed a bug where `:bigint` foreign keys were omitted from the migration.
17
48
 
18
49
  ## [0.1.3] - 2020-10-08
19
50
  ### Changed
@@ -29,6 +60,9 @@ using the appropriate Rails configuration attributes.
29
60
  ### Added
30
61
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
31
62
 
63
+ [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
64
+ [0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
65
+ [0.3.0]: https://github.com/Invoca/declare_schema/compare/v0.2.0...v0.3.0
32
66
  [0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
33
67
  [0.1.3]: https://github.com/Invoca/declare_schema/compare/v0.1.2...v0.1.3
34
68
  [0.1.2]: https://github.com/Invoca/declare_schema/compare/v0.1.1...v0.1.2
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.2.0)
4
+ declare_schema (0.4.0)
5
5
  rails (>= 4.2)
6
6
 
7
7
  GEM
@@ -54,7 +54,7 @@ GEM
54
54
  thor (>= 0.14.0)
55
55
  arel (9.0.0)
56
56
  ast (2.4.1)
57
- bootsnap (1.4.8)
57
+ bootsnap (1.5.0)
58
58
  msgpack (~> 1.0)
59
59
  builder (3.2.4)
60
60
  byebug (11.1.3)
@@ -134,19 +134,19 @@ GEM
134
134
  actionpack (>= 5.0)
135
135
  railties (>= 5.0)
136
136
  rexml (3.2.4)
137
- rspec (3.9.0)
138
- rspec-core (~> 3.9.0)
139
- rspec-expectations (~> 3.9.0)
140
- rspec-mocks (~> 3.9.0)
141
- rspec-core (3.9.2)
142
- rspec-support (~> 3.9.3)
143
- rspec-expectations (3.9.2)
137
+ rspec (3.10.0)
138
+ rspec-core (~> 3.10.0)
139
+ rspec-expectations (~> 3.10.0)
140
+ rspec-mocks (~> 3.10.0)
141
+ rspec-core (3.10.0)
142
+ rspec-support (~> 3.10.0)
143
+ rspec-expectations (3.10.0)
144
144
  diff-lcs (>= 1.2.0, < 2.0)
145
- rspec-support (~> 3.9.0)
146
- rspec-mocks (3.9.1)
145
+ rspec-support (~> 3.10.0)
146
+ rspec-mocks (3.10.0)
147
147
  diff-lcs (>= 1.2.0, < 2.0)
148
- rspec-support (~> 3.9.0)
149
- rspec-support (3.9.3)
148
+ rspec-support (~> 3.10.0)
149
+ rspec-support (3.10.0)
150
150
  rubocop (0.91.0)
151
151
  parallel (~> 1.10)
152
152
  parser (>= 2.7.1.1)
data/README.md CHANGED
@@ -50,6 +50,26 @@ Migration filename: [<enter>=declare_schema_migration_1|<custom_name>]: add_comp
50
50
  ```
51
51
  Note that the migration generator is interactive -- it can't tell the difference between renaming something vs. adding one thing and removing another, so sometimes it will ask you to clarify.
52
52
 
53
+ ## Migrator Configuration
54
+
55
+ The following configuration options are available for the gem and can be used
56
+ during the initialization of your Rails application.
57
+
58
+ ### before_generating_migration callback
59
+
60
+ During the initializtion process for generating migrations, `DeclareSchema` will
61
+ trigger the `eager_load!` on the `Rails` application and all `Rails::Engine`s loaded
62
+ into scope. If you need to generate migrations for models that aren't automatically loaded by `eager_load!`,
63
+ load them in the `before_generating_migration` block.
64
+
65
+ **Example Configuration**
66
+
67
+ ```ruby
68
+ DeclareSchema::Migration::Migrator.before_generating_migration do
69
+ require 'lib/some/hidden/models.rb'
70
+ end
71
+ ```
72
+
53
73
  ## Installing
54
74
 
55
75
  Install the `DeclareSchema` gem directly:
@@ -39,6 +39,7 @@ require 'declare_schema/extensions/active_record/fields_declaration'
39
39
  require 'declare_schema/field_declaration_dsl'
40
40
  require 'declare_schema/model'
41
41
  require 'declare_schema/model/field_spec'
42
- require 'declare_schema/model/index_spec'
42
+ require 'declare_schema/model/index_definition'
43
+ require 'declare_schema/model/foreign_key_definition'
43
44
 
44
45
  require 'declare_schema/railtie' if defined?(Rails)
@@ -25,8 +25,8 @@ module DeclareSchema
25
25
  # and speeds things up a little.
26
26
  inheriting_cattr_reader field_specs: HashWithIndifferentAccess.new
27
27
 
28
- # index_specs holds IndexSpec objects for all the declared indexes.
29
- inheriting_cattr_reader index_specs: []
28
+ # index_definitions holds IndexDefinition objects for all the declared indexes.
29
+ inheriting_cattr_reader index_definitions: []
30
30
  inheriting_cattr_reader ignore_indexes: []
31
31
  inheriting_cattr_reader constraint_specs: []
32
32
 
@@ -51,19 +51,19 @@ module DeclareSchema
51
51
  def index(fields, options = {})
52
52
  # don't double-index fields
53
53
  index_fields_s = Array.wrap(fields).map(&:to_s)
54
- unless index_specs.any? { |index_spec| index_spec.fields == index_fields_s }
55
- index_specs << ::DeclareSchema::Model::IndexSpec.new(self, fields, options)
54
+ unless index_definitions.any? { |index_spec| index_spec.fields == index_fields_s }
55
+ index_definitions << ::DeclareSchema::Model::IndexDefinition.new(self, fields, options)
56
56
  end
57
57
  end
58
58
 
59
59
  def primary_key_index(*fields)
60
- index(fields.flatten, unique: true, name: "PRIMARY_KEY")
60
+ index(fields.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
61
61
  end
62
62
 
63
63
  def constraint(fkey, options = {})
64
64
  fkey_s = fkey.to_s
65
65
  unless constraint_specs.any? { |constraint_spec| constraint_spec.foreign_key == fkey_s }
66
- constraint_specs << DeclareSchema::Model::ForeignKeySpec.new(self, fkey, options)
66
+ constraint_specs << DeclareSchema::Model::ForeignKeyDefinition.new(self, fkey, options)
67
67
  end
68
68
  end
69
69
 
@@ -79,19 +79,20 @@ module DeclareSchema
79
79
  # declarations.
80
80
  def declare_field(name, type, *args)
81
81
  options = args.extract_options!
82
- field_added(name, type, args, options) if respond_to?(:field_added)
83
- add_formatting_for_field(name, type, args)
82
+ try(:field_added, name, type, args, options)
83
+ add_serialize_for_field(name, type, options)
84
+ add_formatting_for_field(name, type)
84
85
  add_validations_for_field(name, type, args, options)
85
86
  add_index_for_field(name, args, options)
86
87
  field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, options)
87
- attr_order << name unless name.in?(attr_order)
88
+ attr_order << name unless attr_order.include?(name)
88
89
  end
89
90
 
90
- def index_specs_with_primary_key
91
- if index_specs.any?(&:primary_key?)
92
- index_specs
91
+ def index_definitions_with_primary_key
92
+ if index_definitions.any?(&:primary_key?)
93
+ index_definitions
93
94
  else
94
- index_specs + [rails_default_primary_key]
95
+ index_definitions + [rails_default_primary_key]
95
96
  end
96
97
  end
97
98
 
@@ -102,21 +103,18 @@ module DeclareSchema
102
103
  private
103
104
 
104
105
  def rails_default_primary_key
105
- ::DeclareSchema::Model::IndexSpec.new(self, [primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexSpec::PRIMARY_KEY_NAME)
106
+ ::DeclareSchema::Model::IndexDefinition.new(self, [primary_key.to_sym], unique: true, name: DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
106
107
  end
107
108
 
108
109
  # Extend belongs_to so that it creates a FieldSpec for the foreign key
109
- def belongs_to(name, *args, &block)
110
- if args.size == 0 || (args.size == 1 && args[0].is_a?(Proc))
111
- options = {}
112
- args.push(options)
113
- elsif args.size == 1
114
- options = args[0]
115
- else
116
- options = args[1]
117
- end
110
+ def belongs_to(name, scope = nil, **options)
118
111
  column_options = {}
119
- column_options[:null] = options.delete(:null) || false
112
+
113
+ column_options[:null] = if options.has_key?(:null)
114
+ options.delete(:null)
115
+ elsif options.has_key?(:optional)
116
+ options[:optional] # infer :null from :optional
117
+ end || false
120
118
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
121
119
  column_options[:limit] = options.delete(:limit) if options.has_key?(:limit)
122
120
 
@@ -129,20 +127,30 @@ module DeclareSchema
129
127
  fk_options[:constraint_name] = options.delete(:constraint) if options.has_key?(:constraint)
130
128
  fk_options[:index_name] = index_options[:name]
131
129
 
130
+ fk = options[:foreign_key]&.to_s || "#{name}_id"
131
+
132
+ if !options.has_key?(:optional)
133
+ options[:optional] = column_options[:null] # infer :optional from :null
134
+ end
135
+
132
136
  fk_options[:dependent] = options.delete(:far_end_dependent) if options.has_key?(:far_end_dependent)
133
- super(name, *args, &block).tap do |_bt|
134
- refl = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
135
- fkey = refl.foreign_key
136
- declare_field(fkey.to_sym, :integer, column_options)
137
- if refl.options[:polymorphic]
138
- foreign_type = options[:foreign_type] || "#{name}_type"
139
- declare_polymorphic_type_field(foreign_type, column_options)
140
- index([foreign_type, fkey], index_options) if index_options[:name] != false
141
- else
142
- index(fkey, index_options) if index_options[:name] != false
143
- options[:constraint_name] = options
144
- constraint(fkey, fk_options) if fk_options[:constraint_name] != false
145
- end
137
+
138
+ if Rails::VERSION::MAJOR >= 5
139
+ super
140
+ else
141
+ super(name, scope, options.except(:optional))
142
+ end
143
+
144
+ refl = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
145
+ fkey = refl.foreign_key or raise "Couldn't find foreign_key for #{name} in #{refl.inspect}"
146
+ declare_field(fkey.to_sym, :integer, column_options)
147
+ if refl.options[:polymorphic]
148
+ foreign_type = options[:foreign_type] || "#{name}_type"
149
+ declare_polymorphic_type_field(foreign_type, column_options)
150
+ index([foreign_type, fkey], index_options) if index_options[:name] != false
151
+ else
152
+ index(fkey, index_options) if index_options[:name] != false
153
+ constraint(fkey, fk_options) if fk_options[:constraint_name] != false
146
154
  end
147
155
  end
148
156
 
@@ -159,9 +167,8 @@ module DeclareSchema
159
167
  # does not effect the attribute in any way - it just records the
160
168
  # metadata.
161
169
  def declare_attr_type(name, type, options = {})
162
- klass = DeclareSchema.to_class(type)
163
- attr_types[name] = DeclareSchema.to_class(type)
164
- klass.declared(self, name, options) if klass.respond_to?(:declared)
170
+ attr_types[name] = klass = DeclareSchema.to_class(type)
171
+ klass.try(:declared, self, name, options)
165
172
  end
166
173
 
167
174
  # Add field validations according to arguments in the
@@ -185,7 +192,37 @@ module DeclareSchema
185
192
  end
186
193
  end
187
194
 
188
- def add_formatting_for_field(name, type, _args)
195
+ def add_serialize_for_field(name, type, options)
196
+ if (serialize_class = options.delete(:serialize))
197
+ type == :string || type == :text or raise ArgumentError, "serialize field type must be :string or :text"
198
+ serialize_args = Array((serialize_class unless serialize_class == true))
199
+ serialize(name, *serialize_args)
200
+ if options.has_key?(:default)
201
+ options[:default] = serialized_default(name, serialize_class == true ? Object : serialize_class, options[:default])
202
+ end
203
+ end
204
+ end
205
+
206
+ def serialized_default(attr_name, class_name_or_coder, default)
207
+ # copied from https://github.com/rails/rails/blob/7d6cb950e7c0e31c2faaed08c81743439156c9f5/activerecord/lib/active_record/attribute_methods/serialization.rb#L70-L76
208
+ coder = if class_name_or_coder == ::JSON
209
+ ActiveRecord::Coders::JSON
210
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
211
+ class_name_or_coder
212
+ elsif Rails::VERSION::MAJOR >= 5
213
+ ActiveRecord::Coders::YAMLColumn.new(attr_name, class_name_or_coder)
214
+ else
215
+ ActiveRecord::Coders::YAMLColumn.new(class_name_or_coder)
216
+ end
217
+
218
+ if default == coder.load(nil)
219
+ nil # handle Array default: [] or Hash default: {}
220
+ else
221
+ coder.dump(default)
222
+ end
223
+ end
224
+
225
+ def add_formatting_for_field(name, type)
189
226
  if (type_class = DeclareSchema.to_class(type))
190
227
  if "format".in?(type_class.instance_methods)
191
228
  before_validation do |record|
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class ForeignKeyDefinition
6
+ include Comparable
7
+
8
+ attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
9
+
10
+ def initialize(model, foreign_key, options = {})
11
+ @model = model
12
+ @foreign_key = foreign_key.presence
13
+ @options = options
14
+
15
+ @child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
16
+ @parent_table_name = options[:parent_table]
17
+ @foreign_key_name = options[:foreign_key] || self.foreign_key
18
+ @index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
19
+ @constraint_name = options[:constraint_name] || @index_name || ''
20
+ @on_delete_cascade = options[:dependent] == :delete
21
+
22
+ # Empty constraint lets mysql generate the name
23
+ end
24
+
25
+ class << self
26
+ def for_model(model, old_table_name)
27
+ show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
28
+ constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
29
+
30
+ constraints.map do |fkc|
31
+ options = {}
32
+ name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
33
+ options[:constraint_name] = name
34
+ options[:parent_table] = parent_table
35
+ options[:foreign_key] = foreign_key
36
+ options[:dependent] = :delete if fkc['ON DELETE CASCADE']
37
+
38
+ new(model, foreign_key, options)
39
+ end
40
+ end
41
+ end
42
+
43
+ def parent_table_name
44
+ @parent_table_name ||=
45
+ if (klass = options[:class_name])
46
+ klass = klass.to_s.constantize unless klass.is_a?(Class)
47
+ klass.try(:table_name)
48
+ end || foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
49
+ end
50
+
51
+ attr_writer :parent_table_name
52
+
53
+ def to_add_statement
54
+ statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
55
+ "execute #{statement.inspect}"
56
+ end
57
+
58
+ def key
59
+ @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
60
+ end
61
+
62
+ def hash
63
+ key.hash
64
+ end
65
+
66
+ def <=>(rhs)
67
+ key <=> rhs.key
68
+ end
69
+
70
+ alias eql? ==
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class IndexDefinition
6
+ include Comparable
7
+
8
+ # TODO: replace `fields` with `columns` and remove alias. -Colin
9
+ attr_reader :table, :fields, :explicit_name, :name, :unique, :where
10
+ alias columns fields
11
+
12
+ class IndexNameTooLongError < RuntimeError; end
13
+
14
+ PRIMARY_KEY_NAME = "PRIMARY"
15
+ MYSQL_INDEX_NAME_MAX_LENGTH = 64
16
+
17
+ def initialize(model, fields, options = {})
18
+ @model = model
19
+ @table = options.delete(:table_name) || model.table_name
20
+ @fields = Array.wrap(fields).map(&:to_s)
21
+ @explicit_name = options[:name] unless options.delete(:allow_equivalent)
22
+ @name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
23
+ @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
24
+
25
+ if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
26
+ raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
27
+ end
28
+
29
+ if (where = options[:where])
30
+ @where = where.start_with?('(') ? where : "(#{where})"
31
+ end
32
+ end
33
+
34
+ class << self
35
+ # extract IndexSpecs from an existing table
36
+ # always includes the PRIMARY KEY index
37
+ def for_model(model, old_table_name = nil)
38
+ t = old_table_name || model.table_name
39
+
40
+ primary_key_columns = Array(model.connection.primary_key(t)).presence || begin
41
+ cols = model.connection.columns(t)
42
+ Array(
43
+ if cols.any? { |col| col.name == 'id' }
44
+ 'id'
45
+ else
46
+ cols.find { |col| col.type.to_s.include?('int') }&.name or raise "could not guess primary key for #{t} in #{cols.inspect}"
47
+ end
48
+ )
49
+ end
50
+ primary_key_found = false
51
+ index_definitions = model.connection.indexes(t).map do |i|
52
+ model.ignore_indexes.include?(i.name) and next
53
+ if i.name == PRIMARY_KEY_NAME
54
+ i.columns == primary_key_columns && i.unique or
55
+ raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
56
+ primary_key_found = true
57
+ elsif i.columns == primary_key_columns && i.unique
58
+ raise "found primary key index on #{t}.#{primary_key_columns} but it was called #{i.name}"
59
+ end
60
+ new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
61
+ end.compact
62
+
63
+ if !primary_key_found
64
+ index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
65
+ end
66
+ index_definitions
67
+ end
68
+ end
69
+
70
+ def primary_key?
71
+ name == PRIMARY_KEY_NAME
72
+ end
73
+
74
+ def to_add_statement(new_table_name, existing_primary_key = nil)
75
+ if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
76
+ to_add_primary_key_statement(new_table_name, existing_primary_key)
77
+ else
78
+ # Note: + below keeps that interpolated string from being frozen, so we can << into it.
79
+ r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
80
+ r << ", unique: true" if unique
81
+ r << ", where: '#{where}'" if where.present?
82
+ r << ", name: '#{name}'"
83
+ r
84
+ end
85
+ end
86
+
87
+ def to_add_primary_key_statement(new_table_name, existing_primary_key)
88
+ drop = "DROP PRIMARY KEY, " if existing_primary_key
89
+ statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
90
+ "execute #{statement.inspect}"
91
+ end
92
+
93
+ def to_key
94
+ @key ||= [table, fields, name, unique, where].map(&:to_s)
95
+ end
96
+
97
+ def settings
98
+ @settings ||= [table, fields, unique].map(&:to_s)
99
+ end
100
+
101
+ def hash
102
+ to_key.hash
103
+ end
104
+
105
+ def <=>(rhs)
106
+ to_key <=> rhs.to_key
107
+ end
108
+
109
+ def equivalent?(rhs)
110
+ settings == rhs.settings
111
+ end
112
+
113
+ def with_name(new_name)
114
+ self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
115
+ end
116
+
117
+ alias eql? ==
118
+ end
119
+ end
120
+ end