declare_schema 0.3.0.pre.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a82a5e54f5dadf8a733520efe057d8b1a0319e1725ef82d41d97004ee68cf0c
4
- data.tar.gz: 899e9f135706d4883b448bcffc09b5f962cb84892c1bf1be27e6f2e36209454f
3
+ metadata.gz: 2e1d3c68c4e9b2f6ce739eba37ed19c5f80204495a50a9240e9f1f68a09777e3
4
+ data.tar.gz: 63e0a3205c82c2b5e6dacbfbfd10e9aa2e63eee5898d11fd5c102618e73755aa
5
5
  SHA512:
6
- metadata.gz: 0465ffd5522605f9e0c61beffc2571ab76d077020596d82fe616c60cd631c6663b399c9c5d02c8983d9060223f3bb100932baf411efa69a3a7f236097784ae9e
7
- data.tar.gz: 4c6b7b49067650cd0791257614dc84e1f6f6a9191346578249fa196dc0d9f621c491d597aaa32f82c64bde8f6e7fe8e18f1974f89c4677ade6c8203aede20ec4
6
+ metadata.gz: 34ab74d97dd53c426d3a289b622670a42846d84279c2e3967ad0663c8f4871b1470a90b309237b09b09724a7e7c2405fb0f614a00d33902f8000bb1df23459ef
7
+ data.tar.gz: 2bf6ca852f71807e362ef26b4ba14d75b8a452a6245094eb8023fa1889b94d6584cf15856962f9fb85168208e0de4dd23a7942fb18cdcc5335f7c744382ba826
@@ -4,12 +4,41 @@ 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.1] - 2020-12-04
8
+ ### Fixed
9
+ - Fixed a bug detecting compound primary keys in Rails 4.
10
+
11
+ ## [0.4.0] - 2020-11-20
12
+ ### Added
13
+ - Fields may be declared with `serialize: true` (any value with a valid `.to_yaml` stored as YAML),
14
+ or `serialize: <serializeable-class>`, where `<serializeable-class>`
15
+ 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)
16
+ or any custom serializable class.
17
+ This invokes `ActiveSupport`'s `serialize` macro for that field, passing the serializable class, if given.
18
+
19
+ Note: when `serialize:` is used, any `default:` should be given in a matching Ruby type--for example, `[]` or `{}` or `{ 'currency' => 'USD' }`--in
20
+ which case the serializeable class will be used to determine the serialized default value and that will be set as the SQL default.
21
+
22
+ ### Fixed
23
+ - Sqlite now correctly infers the PRIMARY KEY so it won't attempt to add that index again.
24
+
25
+ ## [0.3.1] - 2020-11-13
26
+ ### Fixed
27
+ - When passing `belongs_to` to Rails, suppress the `optional:` option in Rails 4, since that option was added in Rails 5.
28
+
29
+ ## [0.3.0] - 2020-11-02
8
30
  ### Added
9
31
  - Added support for `belongs_to optional:`.
10
32
  If given, it is passed through to `ActiveRecord`'s `belong_to`.
11
33
  If not given in Rails 5+, the `optional:` value is set equal to the `null:` value (default: `false`) and that
12
- is is passed to `ActiveRecord`'s `belong_to`.
34
+ is passed to `ActiveRecord`'s `belong_to`.
35
+ Similarly, if `null:` is not given, it is inferred from `optional:`.
36
+ If both are given, their values are respected, even if contradictory;
37
+ this is a legitimate case when migrating to/from an optional association.
38
+ - Added a new callback `before_generating_migration` to the `Migrator` that can be
39
+ defined in order to custom load more models that might be missed by `eager_load!`
40
+ ### Fixed
41
+ - 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).
13
42
 
14
43
  ## [0.2.0] - 2020-10-26
15
44
  ### Added
@@ -20,7 +49,7 @@ is is passed to `ActiveRecord`'s `belong_to`.
20
49
 
21
50
  ### Fixed
22
51
  - Fixed a bug where `:text limit: 0xffff_ffff` (max size) was omitted from migrations.
23
- - Fixed a bug where `:bigint` foreign keys were omitted from the migration.
52
+ - Fixed a bug where `:bigint` foreign keys were omitted from the migration.
24
53
 
25
54
  ## [0.1.3] - 2020-10-08
26
55
  ### Changed
@@ -31,11 +60,13 @@ using the appropriate Rails configuration attributes.
31
60
  ### Changed
32
61
  - Added travis support and created 2 specs as a starting point.
33
62
 
34
-
35
63
  ## [0.1.1] - 2020-09-24
36
64
  ### Added
37
65
  - Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
38
66
 
67
+ [0.4.1]: https://github.com/Invoca/declare_schema/compare/v0.4.0...v0.4.1
68
+ [0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
69
+ [0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
39
70
  [0.3.0]: https://github.com/Invoca/declare_schema/compare/v0.2.0...v0.3.0
40
71
  [0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
41
72
  [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.1)
4
+ declare_schema (0.4.1)
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)
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,13 +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, scope = nil, **options, &block)
110
+ def belongs_to(name, scope = nil, **options)
110
111
  column_options = {}
111
- 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
112
118
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
113
119
  column_options[:limit] = options.delete(:limit) if options.has_key?(:limit)
114
120
 
@@ -123,13 +129,17 @@ module DeclareSchema
123
129
 
124
130
  fk = options[:foreign_key]&.to_s || "#{name}_id"
125
131
 
126
- if !options.has_key?(:optional) && Rails::VERSION::MAJOR >= 5
127
- options[:optional] = column_options[:null]
132
+ if !options.has_key?(:optional)
133
+ options[:optional] = column_options[:null] # infer :optional from :null
128
134
  end
129
135
 
130
136
  fk_options[:dependent] = options.delete(:far_end_dependent) if options.has_key?(:far_end_dependent)
131
137
 
132
- super(name, scope, options)
138
+ if Rails::VERSION::MAJOR >= 5
139
+ super
140
+ else
141
+ super(name, scope, options.except(:optional))
142
+ end
133
143
 
134
144
  refl = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
135
145
  fkey = refl.foreign_key or raise "Couldn't find foreign_key for #{name} in #{refl.inspect}"
@@ -140,7 +150,6 @@ module DeclareSchema
140
150
  index([foreign_type, fkey], index_options) if index_options[:name] != false
141
151
  else
142
152
  index(fkey, index_options) if index_options[:name] != false
143
- options[:constraint_name] = options
144
153
  constraint(fkey, fk_options) if fk_options[:constraint_name] != false
145
154
  end
146
155
  end
@@ -158,9 +167,8 @@ module DeclareSchema
158
167
  # does not effect the attribute in any way - it just records the
159
168
  # metadata.
160
169
  def declare_attr_type(name, type, options = {})
161
- klass = DeclareSchema.to_class(type)
162
- attr_types[name] = DeclareSchema.to_class(type)
163
- 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)
164
172
  end
165
173
 
166
174
  # Add field validations according to arguments in the
@@ -184,7 +192,37 @@ module DeclareSchema
184
192
  end
185
193
  end
186
194
 
187
- 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)
188
226
  if (type_class = DeclareSchema.to_class(type))
189
227
  if "format".in?(type_class.instance_methods)
190
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 SQLite
66
+ def sqlite_compound_primary_key(model, table)
67
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) 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