declare_schema 0.3.0.pre.2 → 0.4.2
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 +4 -4
- data/CHANGELOG.md +40 -2
- data/Gemfile.lock +14 -14
- data/lib/declare_schema.rb +2 -1
- data/lib/declare_schema/model.rb +78 -41
- data/lib/declare_schema/model/foreign_key_definition.rb +73 -0
- data/lib/declare_schema/model/index_definition.rb +138 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +15 -15
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/spec/lib/declare_schema/api_spec.rb +7 -8
- data/spec/lib/declare_schema/generator_spec.rb +51 -10
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +16 -14
- data/spec/lib/declare_schema/migration_generator_spec.rb +618 -208
- data/spec/lib/declare_schema/model/index_definition_spec.rb +123 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +2 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/acceptance_spec_helpers.rb +57 -0
- metadata +9 -6
- data/lib/declare_schema/model/index_spec.rb +0 -175
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf6d8bf1670910eeadc3f7ed08860af6630f1b5edc271a0740fe8bb94e7f5c71
|
4
|
+
data.tar.gz: 4d058e02cb6b269f6b66fa20f8ac661ecdb7f8b31547cf43e65e27c869221902
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d95d741db5f9279cfe39f7bd972581708a0ccae7e2d9d209318282a9980d2980e3611592ba08b6b53136e23b92ac4aaa51e2a66187002de04bf818c29715820f
|
7
|
+
data.tar.gz: 93c203bb2ff2255f014d76a3069d1530d97f9603293738844e2faf6f2c9cb6fb95dcc2aa9ad505df15c8e1e14567e076a5e2901b7b4847f804ce3dd85dc5b313
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
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
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
declare_schema (0.
|
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.
|
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.
|
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.
|
138
|
-
rspec-core (~> 3.
|
139
|
-
rspec-expectations (~> 3.
|
140
|
-
rspec-mocks (~> 3.
|
141
|
-
rspec-core (3.
|
142
|
-
rspec-support (~> 3.
|
143
|
-
rspec-expectations (3.
|
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.
|
146
|
-
rspec-mocks (3.
|
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.
|
149
|
-
rspec-support (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/lib/declare_schema.rb
CHANGED
@@ -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/
|
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)
|
data/lib/declare_schema/model.rb
CHANGED
@@ -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
|
-
#
|
29
|
-
inheriting_cattr_reader
|
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
|
55
|
-
|
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:
|
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::
|
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
|
83
|
-
|
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
|
88
|
+
attr_order << name unless attr_order.include?(name)
|
88
89
|
end
|
89
90
|
|
90
|
-
def
|
91
|
-
if
|
92
|
-
|
91
|
+
def index_definitions_with_primary_key
|
92
|
+
if index_definitions.any?(&:primary_key?)
|
93
|
+
index_definitions
|
93
94
|
else
|
94
|
-
|
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::
|
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,
|
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
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
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
|
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
|