declare_schema 0.2.0 → 0.4.0

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