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 +4 -4
- data/CHANGELOG.md +35 -4
- data/Gemfile.lock +14 -14
- data/README.md +20 -0
- data/lib/declare_schema.rb +2 -1
- data/lib/declare_schema/model.rb +62 -24
- 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 +26 -21
- 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 +594 -233
- data/spec/lib/declare_schema/model/index_definition_spec.rb +123 -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: 2e1d3c68c4e9b2f6ce739eba37ed19c5f80204495a50a9240e9f1f68a09777e3
|
4
|
+
data.tar.gz: 63e0a3205c82c2b5e6dacbfbfd10e9aa2e63eee5898d11fd5c102618e73755aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34ab74d97dd53c426d3a289b622670a42846d84279c2e3967ad0663c8f4871b1470a90b309237b09b09724a7e7c2405fb0f614a00d33902f8000bb1df23459ef
|
7
|
+
data.tar.gz: 2bf6ca852f71807e362ef26b4ba14d75b8a452a6245094eb8023fa1889b94d6584cf15856962f9fb85168208e0de4dd23a7942fb18cdcc5335f7c744382ba826
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
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
|
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
|
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.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.
|
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/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,13 +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, scope = nil, **options
|
110
|
+
def belongs_to(name, scope = nil, **options)
|
110
111
|
column_options = {}
|
111
|
-
|
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)
|
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
|
-
|
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
|
-
|
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
|
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
|