declare_schema 0.3.0.pre.2 → 0.4.2

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: 3364c39b6579bb69804417a10b5b287208e6fdfe5a23bf57b6b3f928f69fe539
4
- data.tar.gz: 2fbc6abab32d750ef1af34d6888b5954ee27da2563acd8595728d3d23d184244
3
+ metadata.gz: bf6d8bf1670910eeadc3f7ed08860af6630f1b5edc271a0740fe8bb94e7f5c71
4
+ data.tar.gz: 4d058e02cb6b269f6b66fa20f8ac661ecdb7f8b31547cf43e65e27c869221902
5
5
  SHA512:
6
- metadata.gz: 7e884c085c0d72f1146d7b7f52abbf1f44b19389e2f96e2858b855b11041c94406b0be159a300366d0abeec2f36cf9056d3eddd61352600c780a992ca3971817
7
- data.tar.gz: 1d45f652bd8d0de74b71fec913c4005be8f3212de7fa7942ad587d67059f67115ce8d797be9d71121e792ab66aee84c2adaaa013766b6d96cb21eda1d35522b4
6
+ metadata.gz: d95d741db5f9279cfe39f7bd972581708a0ccae7e2d9d209318282a9980d2980e3611592ba08b6b53136e23b92ac4aaa51e2a66187002de04bf818c29715820f
7
+ data.tar.gz: 93c203bb2ff2255f014d76a3069d1530d97f9603293738844e2faf6f2c9cb6fb95dcc2aa9ad505df15c8e1e14567e076a5e2901b7b4847f804ce3dd85dc5b313
@@ -4,10 +4,45 @@ 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.3.0] - Unreleased
7
+ ## [0.4.2] - 2020-12-05
8
+ ### Fixed
9
+ - Generalize the fix below to sqlite || Rails 4.
10
+
11
+ ## [0.4.1] - 2020-12-04
12
+ ### Fixed
13
+ - Fixed a bug detecting compound primary keys in Rails 4.
14
+
15
+ ## [0.4.0] - 2020-11-20
8
16
  ### Added
17
+ - Fields may be declared with `serialize: true` (any value with a valid `.to_yaml` stored as YAML),
18
+ or `serialize: <serializeable-class>`, where `<serializeable-class>`
19
+ may be `Array` (`Array` stored as YAML) or `Hash` (`Hash` stored as YAML) or `JSON` (any value with a valid `.to_json`, stored as JSON)
20
+ or any custom serializable class.
21
+ This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
22
+
23
+ Note: when `serialize:` is used, any `default:` should be given in a matching Ruby type--for example, `[]` or `{}` or `{ 'currency' => 'USD' }`--in
24
+ which case the serializeable class will be used to determine the serialized default value and that will be set as the SQL default.
25
+
26
+ ### Fixed
27
+ - Sqlite now correctly infers the PRIMARY KEY so it won't attempt to add that index again.
28
+
29
+ ## [0.3.1] - 2020-11-13
30
+ ### Fixed
31
+ - When passing `belongs_to` to Rails, suppress the `optional:` option in Rails 4, since that option was added in Rails 5.
32
+
33
+ ## [0.3.0] - 2020-11-02
34
+ ### Added
35
+ - Added support for `belongs_to optional:`.
36
+ If given, it is passed through to `ActiveRecord`'s `belong_to`.
37
+ If not given in Rails 5+, the `optional:` value is set equal to the `null:` value (default: `false`) and that
38
+ is passed to `ActiveRecord`'s `belong_to`.
39
+ Similarly, if `null:` is not given, it is inferred from `optional:`.
40
+ If both are given, their values are respected, even if contradictory;
41
+ this is a legitimate case when migrating to/from an optional association.
9
42
  - Added a new callback `before_generating_migration` to the `Migrator` that can be
10
43
  defined in order to custom load more models that might be missed by `eager_load!`
44
+ ### Fixed
45
+ - 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).
11
46
 
12
47
  ## [0.2.0] - 2020-10-26
13
48
  ### Added
@@ -29,11 +64,14 @@ using the appropriate Rails configuration attributes.
29
64
  ### Changed
30
65
  - Added travis support and created 2 specs as a starting point.
31
66
 
32
-
33
67
  ## [0.1.1] - 2020-09-24
34
68
  ### Added
35
69
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
36
70
 
71
+ [0.4.2]: https://github.com/Invoca/declare_schema/compare/v0.4.1...v0.4.2
72
+ [0.4.1]: https://github.com/Invoca/declare_schema/compare/v0.4.0...v0.4.1
73
+ [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
74
+ [0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
37
75
  [0.3.0]: https://github.com/Invoca/declare_schema/compare/v0.2.0...v0.3.0
38
76
  [0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
39
77
  [0.1.3]: https://github.com/Invoca/declare_schema/compare/v0.1.2...v0.1.3
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- declare_schema (0.3.0.pre.2)
4
+ declare_schema (0.4.2)
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.1)
58
58
  msgpack (~> 1.0)
59
59
  builder (3.2.4)
60
60
  byebug (11.1.3)
@@ -69,7 +69,7 @@ GEM
69
69
  activesupport (>= 4.2.0)
70
70
  i18n (1.8.5)
71
71
  concurrent-ruby (~> 1.0)
72
- listen (3.2.1)
72
+ listen (3.3.1)
73
73
  rb-fsevent (~> 0.10, >= 0.10.3)
74
74
  rb-inotify (~> 0.9, >= 0.9.10)
75
75
  loofah (2.7.0)
@@ -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)
@@ -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,138 @@
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 || sqlite_compound_primary_key(model, t) or
41
+ raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
42
+
43
+ primary_key_found = false
44
+ index_definitions = model.connection.indexes(t).map do |i|
45
+ model.ignore_indexes.include?(i.name) and next
46
+ if i.name == PRIMARY_KEY_NAME
47
+ i.columns == primary_key_columns && i.unique or
48
+ raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
49
+ primary_key_found = true
50
+ elsif i.columns == primary_key_columns && i.unique
51
+ # skip this primary key index since we'll create it below, with PRIMARY_KEY_NAME
52
+ next
53
+ end
54
+ new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
55
+ end.compact
56
+
57
+ if !primary_key_found
58
+ index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
59
+ end
60
+ index_definitions
61
+ end
62
+
63
+ private
64
+
65
+ # This is the old approach which is still needed for MySQL in Rails 4 and SQLite
66
+ def sqlite_compound_primary_key(model, table)
67
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) || Rails::VERSION::MAJOR < 5 or return nil
68
+
69
+ connection = model.connection.dup
70
+
71
+ class << connection # defeat Rails MySQL driver code that skips the primary key by changing its name to a symbol
72
+ def each_hash(result)
73
+ super do |hash|
74
+ if hash[:Key_name] == PRIMARY_KEY_NAME
75
+ hash[:Key_name] = PRIMARY_KEY_NAME.to_sym
76
+ end
77
+ yield hash
78
+ end
79
+ end
80
+ end
81
+
82
+ pk_index = connection.indexes(table).find { |index| index.name.to_s == PRIMARY_KEY_NAME } or return nil
83
+
84
+ Array(pk_index.columns)
85
+ end
86
+ end
87
+
88
+ def primary_key?
89
+ name == PRIMARY_KEY_NAME
90
+ end
91
+
92
+ def to_add_statement(new_table_name, existing_primary_key = nil)
93
+ if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
94
+ to_add_primary_key_statement(new_table_name, existing_primary_key)
95
+ else
96
+ # Note: + below keeps that interpolated string from being frozen, so we can << into it.
97
+ r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
98
+ r << ", unique: true" if unique
99
+ r << ", where: '#{where}'" if where.present?
100
+ r << ", name: '#{name}'"
101
+ r
102
+ end
103
+ end
104
+
105
+ def to_add_primary_key_statement(new_table_name, existing_primary_key)
106
+ drop = "DROP PRIMARY KEY, " if existing_primary_key
107
+ statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
108
+ "execute #{statement.inspect}"
109
+ end
110
+
111
+ def to_key
112
+ @key ||= [table, fields, name, unique, where].map(&:to_s)
113
+ end
114
+
115
+ def settings
116
+ @settings ||= [table, fields, unique].map(&:to_s)
117
+ end
118
+
119
+ def hash
120
+ to_key.hash
121
+ end
122
+
123
+ def <=>(rhs)
124
+ to_key <=> rhs.to_key
125
+ end
126
+
127
+ def equivalent?(rhs)
128
+ settings == rhs.settings
129
+ end
130
+
131
+ def with_name(new_name)
132
+ self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
133
+ end
134
+
135
+ alias eql? ==
136
+ end
137
+ end
138
+ end