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 +4 -4
- data/CHANGELOG.md +35 -1
- data/Gemfile.lock +13 -13
- data/README.md +20 -0
- 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 +120 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +26 -21
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/spec/lib/declare_schema/api_spec.rb +6 -7
- 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 +613 -208
- data/spec/lib/declare_schema/model/index_definition_spec.rb +83 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +2 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +30 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/acceptance_spec_helpers.rb +57 -0
- metadata +6 -3
- 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: 1e5c665ec203dd84e445290bac1de190491560ddc409fe2bd3f5ba4831e065c5
|
4
|
+
data.tar.gz: 244148b892d2de22a692c4db50b136995f97d8da38573e87a443303fdee5be7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44994fd571b97768ffec2c9bbce779b8beef222ba50213a425a3021c66f1dc4e4fd7a50adc95cf3d1a1116e67eaf5c47b11e635408856c726118a22c18188b98
|
7
|
+
data.tar.gz: 7d3eb3be095a1dce65b6b39b35974a6c96ee940fec31dba3fddbfb528be8be3b42906ec81c6075cde5d8fa590df4afc8ca12887a956c772fc34c084791577526
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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.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.
|
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.
|
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/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:
|
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,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
|