declare_schema 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|