declare_schema 0.3.1 → 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 +14 -0
- data/Gemfile.lock +1 -1
- data/lib/declare_schema.rb +2 -1
- data/lib/declare_schema/model.rb +48 -18
- 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 +14 -14
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +14 -12
- data/spec/lib/declare_schema/migration_generator_spec.rb +204 -23
- data/spec/lib/declare_schema/model/index_definition_spec.rb +83 -0
- metadata +5 -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,19 @@ 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
|
+
|
7
20
|
## [0.3.1] - 2020-11-13
|
8
21
|
### Fixed
|
9
22
|
- When passing `belongs_to` to Rails, suppress the `optional:` option in Rails 4, since that option was added in Rails 5.
|
@@ -47,6 +60,7 @@ using the appropriate Rails configuration attributes.
|
|
47
60
|
### Added
|
48
61
|
- Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
|
49
62
|
|
63
|
+
[0.4.0]: https://github.com/Invoca/declare_schema/compare/v0.3.1...v0.4.0
|
50
64
|
[0.3.1]: https://github.com/Invoca/declare_schema/compare/v0.3.0...v0.3.1
|
51
65
|
[0.3.0]: https://github.com/Invoca/declare_schema/compare/v0.2.0...v0.3.0
|
52
66
|
[0.2.0]: https://github.com/Invoca/declare_schema/compare/v0.1.3...v0.2.0
|
data/Gemfile.lock
CHANGED
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,7 +103,7 @@ 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
|
@@ -166,9 +167,8 @@ module DeclareSchema
|
|
166
167
|
# does not effect the attribute in any way - it just records the
|
167
168
|
# metadata.
|
168
169
|
def declare_attr_type(name, type, options = {})
|
169
|
-
klass = DeclareSchema.to_class(type)
|
170
|
-
|
171
|
-
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)
|
172
172
|
end
|
173
173
|
|
174
174
|
# Add field validations according to arguments in the
|
@@ -192,7 +192,37 @@ module DeclareSchema
|
|
192
192
|
end
|
193
193
|
end
|
194
194
|
|
195
|
-
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)
|
196
226
|
if (type_class = DeclareSchema.to_class(type))
|
197
227
|
if "format".in?(type_class.instance_methods)
|
198
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
|
@@ -45,14 +45,14 @@ module Generators
|
|
45
45
|
false # no single-column primary key
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
48
|
+
def index_definitions_with_primary_key
|
49
49
|
[
|
50
|
-
::DeclareSchema::Model::
|
51
|
-
::DeclareSchema::Model::
|
50
|
+
::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME),
|
51
|
+
::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
|
52
52
|
]
|
53
53
|
end
|
54
54
|
|
55
|
-
alias_method :
|
55
|
+
alias_method :index_definitions, :index_definitions_with_primary_key
|
56
56
|
|
57
57
|
def ignore_indexes
|
58
58
|
[]
|
@@ -60,8 +60,8 @@ module Generators
|
|
60
60
|
|
61
61
|
def constraint_specs
|
62
62
|
[
|
63
|
-
::DeclareSchema::Model::
|
64
|
-
::DeclareSchema::Model::
|
63
|
+
::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
|
64
|
+
::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
|
65
65
|
]
|
66
66
|
end
|
67
67
|
end
|
@@ -341,7 +341,7 @@ module Generators
|
|
341
341
|
end
|
342
342
|
|
343
343
|
def create_indexes(model)
|
344
|
-
model.
|
344
|
+
model.index_definitions.map { |i| i.to_add_statement(model.table_name) }
|
345
345
|
end
|
346
346
|
|
347
347
|
def create_constraints(model)
|
@@ -449,8 +449,8 @@ module Generators
|
|
449
449
|
return [[], []] if Migrator.disable_constraints
|
450
450
|
|
451
451
|
new_table_name = model.table_name
|
452
|
-
existing_indexes = ::DeclareSchema::Model::
|
453
|
-
model_indexes_with_equivalents = model.
|
452
|
+
existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
|
453
|
+
model_indexes_with_equivalents = model.index_definitions_with_primary_key
|
454
454
|
model_indexes = model_indexes_with_equivalents.map do |i|
|
455
455
|
if i.explicit_name.nil?
|
456
456
|
if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
|
@@ -458,20 +458,20 @@ module Generators
|
|
458
458
|
end
|
459
459
|
end || i
|
460
460
|
end
|
461
|
-
existing_has_primary_key = existing_indexes.any? { |i| i.name ==
|
462
|
-
model_has_primary_key = model_indexes.any? { |i| i.name ==
|
461
|
+
existing_has_primary_key = existing_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
462
|
+
model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
463
463
|
|
464
464
|
add_indexes_init = model_indexes - existing_indexes
|
465
465
|
drop_indexes_init = existing_indexes - model_indexes
|
466
466
|
undo_add_indexes = []
|
467
467
|
undo_drop_indexes = []
|
468
468
|
add_indexes = add_indexes_init.map do |i|
|
469
|
-
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name ==
|
469
|
+
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
470
470
|
i.to_add_statement(new_table_name, existing_has_primary_key)
|
471
471
|
end
|
472
472
|
drop_indexes = drop_indexes_init.map do |i|
|
473
473
|
undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
|
474
|
-
drop_index(new_table_name, i.name) unless i.name ==
|
474
|
+
drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
475
475
|
end.compact
|
476
476
|
|
477
477
|
# the order is important here - adding a :unique, for instance needs to remove then add
|
@@ -489,7 +489,7 @@ module Generators
|
|
489
489
|
return [[], []] if Migrator.disable_indexing
|
490
490
|
|
491
491
|
new_table_name = model.table_name
|
492
|
-
existing_fks = ::DeclareSchema::Model::
|
492
|
+
existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
|
493
493
|
model_fks = model.constraint_specs
|
494
494
|
add_fks = model_fks - existing_fks
|
495
495
|
drop_fks = existing_fks - model_fks
|
@@ -31,21 +31,23 @@ RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
|
|
31
31
|
|
32
32
|
### migrate to
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
if Rails::VERSION::MAJOR >= 5
|
35
|
+
# rename to custom primary_key
|
36
|
+
class Foo < ActiveRecord::Base
|
37
|
+
fields do
|
38
|
+
end
|
39
|
+
self.primary_key = "foo_id"
|
37
40
|
end
|
38
|
-
self.primary_key = "foo_id"
|
39
|
-
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
43
|
+
generate_migrations '-n', '-m'
|
44
|
+
expect(Foo.primary_key).to eq('foo_id')
|
44
45
|
|
45
|
-
|
46
|
+
### ensure it doesn't cause further migrations
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
48
|
+
# check no further migrations
|
49
|
+
up = Generators::DeclareSchema::Migration::Migrator.run.first
|
50
|
+
expect(up).to eq("")
|
51
|
+
end
|
50
52
|
end
|
51
53
|
end
|
@@ -51,19 +51,19 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
Advert.connection.schema_cache.clear!
|
55
|
+
Advert.reset_column_information
|
56
|
+
|
54
57
|
expect(migrate).to(
|
55
58
|
migrate_up(<<~EOS.strip)
|
56
59
|
add_column :adverts, :body, :text
|
57
60
|
add_column :adverts, :published_at, :datetime
|
58
|
-
|
59
|
-
add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
|
60
61
|
EOS
|
61
62
|
.and migrate_down(<<~EOS.strip)
|
62
63
|
remove_column :adverts, :body
|
63
64
|
remove_column :adverts, :published_at
|
64
65
|
EOS
|
65
66
|
)
|
66
|
-
# TODO: ^ TECH-4975 add_index should not be there
|
67
67
|
|
68
68
|
Advert.field_specs.clear # not normally needed
|
69
69
|
class Advert < ActiveRecord::Base
|
@@ -329,7 +329,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
329
329
|
)
|
330
330
|
|
331
331
|
Advert.field_specs.delete(:category_id)
|
332
|
-
Advert.
|
332
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["category_id"] }
|
333
333
|
|
334
334
|
# If you specify a custom foreign key, the migration generator observes that:
|
335
335
|
|
@@ -348,7 +348,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
348
348
|
)
|
349
349
|
|
350
350
|
Advert.field_specs.delete(:c_id)
|
351
|
-
Advert.
|
351
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["c_id"] }
|
352
352
|
|
353
353
|
# You can avoid generating the index by specifying `index: false`
|
354
354
|
|
@@ -365,7 +365,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
365
365
|
)
|
366
366
|
|
367
367
|
Advert.field_specs.delete(:category_id)
|
368
|
-
Advert.
|
368
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
369
369
|
|
370
370
|
# You can specify the index name with :index
|
371
371
|
|
@@ -384,7 +384,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
384
384
|
)
|
385
385
|
|
386
386
|
Advert.field_specs.delete(:category_id)
|
387
|
-
Advert.
|
387
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["category_id"] }
|
388
388
|
|
389
389
|
### Timestamps and Optimimistic Locking
|
390
390
|
|
@@ -433,7 +433,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
433
433
|
EOS
|
434
434
|
)
|
435
435
|
|
436
|
-
Advert.
|
436
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
437
437
|
|
438
438
|
# You can ask for a unique index
|
439
439
|
|
@@ -451,7 +451,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
451
451
|
EOS
|
452
452
|
)
|
453
453
|
|
454
|
-
Advert.
|
454
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
455
455
|
|
456
456
|
# You can specify the name for the index
|
457
457
|
|
@@ -469,7 +469,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
469
469
|
EOS
|
470
470
|
)
|
471
471
|
|
472
|
-
Advert.
|
472
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title"] }
|
473
473
|
|
474
474
|
# You can ask for an index outside of the fields block
|
475
475
|
|
@@ -485,7 +485,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
485
485
|
EOS
|
486
486
|
)
|
487
487
|
|
488
|
-
Advert.
|
488
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
489
489
|
|
490
490
|
# The available options for the index function are `:unique` and `:name`
|
491
491
|
|
@@ -501,7 +501,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
501
501
|
EOS
|
502
502
|
)
|
503
503
|
|
504
|
-
Advert.
|
504
|
+
Advert.index_definitions.delete_if { |spec| spec.fields == ["title"] }
|
505
505
|
|
506
506
|
# You can create an index on more than one field
|
507
507
|
|
@@ -517,7 +517,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
517
517
|
EOS
|
518
518
|
)
|
519
519
|
|
520
|
-
Advert.
|
520
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["title", "category_id"] }
|
521
521
|
|
522
522
|
# Finally, you can specify that the migration generator should completely ignore an
|
523
523
|
# index by passing its name to ignore_index in the model.
|
@@ -545,7 +545,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
545
545
|
add_column :ads, :title, :string, limit: 255
|
546
546
|
add_column :ads, :body, :text
|
547
547
|
|
548
|
-
add_index :ads, [:id], unique: true, name: '
|
548
|
+
add_index :ads, [:id], unique: true, name: 'PRIMARY'
|
549
549
|
EOS
|
550
550
|
.and migrate_down(<<~EOS.strip)
|
551
551
|
remove_column :ads, :title
|
@@ -553,7 +553,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
553
553
|
|
554
554
|
rename_table :ads, :adverts
|
555
555
|
|
556
|
-
add_index :adverts, [:id], unique: true, name: '
|
556
|
+
add_index :adverts, [:id], unique: true, name: 'PRIMARY'
|
557
557
|
EOS
|
558
558
|
)
|
559
559
|
|
@@ -588,7 +588,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
588
588
|
add_column :advertisements, :body, :text
|
589
589
|
remove_column :advertisements, :name
|
590
590
|
|
591
|
-
add_index :advertisements, [:id], unique: true, name: '
|
591
|
+
add_index :advertisements, [:id], unique: true, name: 'PRIMARY'
|
592
592
|
EOS
|
593
593
|
.and migrate_down(<<~EOS.strip)
|
594
594
|
remove_column :advertisements, :title
|
@@ -597,7 +597,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
597
597
|
|
598
598
|
rename_table :advertisements, :adverts
|
599
599
|
|
600
|
-
add_index :adverts, [:id], unique: true, name: '
|
600
|
+
add_index :adverts, [:id], unique: true, name: 'PRIMARY'
|
601
601
|
EOS
|
602
602
|
)
|
603
603
|
|
@@ -614,14 +614,11 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
614
614
|
t.integer "id", limit: 8
|
615
615
|
t.string "name", limit: 255
|
616
616
|
end
|
617
|
-
|
618
|
-
add_index "adverts", ["id"], name: "PRIMARY_KEY", unique: true
|
619
617
|
EOS
|
620
618
|
|
621
619
|
rails5_table_create = <<~EOS.strip
|
622
620
|
create_table "adverts", id: :integer, force: :cascade do |t|
|
623
621
|
t.string "name", limit: 255
|
624
|
-
t.index ["id"], name: "PRIMARY_KEY", unique: true
|
625
622
|
end
|
626
623
|
EOS
|
627
624
|
|
@@ -670,7 +667,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
670
667
|
Advert.field_specs.delete(:type)
|
671
668
|
nuke_model_class(SuperFancyAdvert)
|
672
669
|
nuke_model_class(FancyAdvert)
|
673
|
-
Advert.
|
670
|
+
Advert.index_definitions.delete_if { |spec| spec.fields==["type"] }
|
674
671
|
|
675
672
|
## Coping with multiple changes
|
676
673
|
|
@@ -728,7 +725,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
728
725
|
add_column :ads, :created_at, :datetime, null: false
|
729
726
|
change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
|
730
727
|
|
731
|
-
add_index :ads, [:id], unique: true, name: '
|
728
|
+
add_index :ads, [:id], unique: true, name: 'PRIMARY'
|
732
729
|
EOS
|
733
730
|
)
|
734
731
|
|
@@ -756,7 +753,7 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
756
753
|
migrate_up(<<~EOS.strip)
|
757
754
|
rename_column :adverts, :id, :advert_id
|
758
755
|
|
759
|
-
add_index :adverts, [:advert_id], unique: true, name: '
|
756
|
+
add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY'
|
760
757
|
EOS
|
761
758
|
)
|
762
759
|
|
@@ -797,6 +794,190 @@ RSpec.describe 'DeclareSchema Migration Generator' do
|
|
797
794
|
expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
|
798
795
|
end
|
799
796
|
|
797
|
+
describe 'serialize' do
|
798
|
+
before do
|
799
|
+
class Ad < ActiveRecord::Base
|
800
|
+
@serialize_args = []
|
801
|
+
|
802
|
+
class << self
|
803
|
+
attr_reader :serialize_args
|
804
|
+
|
805
|
+
def serialize(*args)
|
806
|
+
@serialize_args << args
|
807
|
+
end
|
808
|
+
end
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
describe 'untyped' do
|
813
|
+
it 'allows serialize: true' do
|
814
|
+
class Ad < ActiveRecord::Base
|
815
|
+
fields do
|
816
|
+
allow_list :text, limit: 0xFFFF, serialize: true
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
expect(Ad.serialize_args).to eq([[:allow_list]])
|
821
|
+
end
|
822
|
+
|
823
|
+
it 'converts defaults with .to_yaml' do
|
824
|
+
class Ad < ActiveRecord::Base
|
825
|
+
fields do
|
826
|
+
allow_list :string, limit: 255, serialize: true, null: true, default: []
|
827
|
+
allow_hash :string, limit: 255, serialize: true, null: true, default: {}
|
828
|
+
allow_string :string, limit: 255, serialize: true, null: true, default: ['abc']
|
829
|
+
allow_null :string, limit: 255, serialize: true, null: true, default: nil
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
expect(Ad.field_specs['allow_list'].default).to eq("--- []\n")
|
834
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("--- {}\n")
|
835
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
836
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
describe 'Array' do
|
841
|
+
it 'allows serialize: Array' do
|
842
|
+
class Ad < ActiveRecord::Base
|
843
|
+
fields do
|
844
|
+
allow_list :string, limit: 255, serialize: Array, null: true
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Array]])
|
849
|
+
end
|
850
|
+
|
851
|
+
it 'allows Array defaults' do
|
852
|
+
class Ad < ActiveRecord::Base
|
853
|
+
fields do
|
854
|
+
allow_list :string, limit: 255, serialize: Array, null: true, default: [2]
|
855
|
+
allow_string :string, limit: 255, serialize: Array, null: true, default: ['abc']
|
856
|
+
allow_empty :string, limit: 255, serialize: Array, null: true, default: []
|
857
|
+
allow_null :string, limit: 255, serialize: Array, null: true, default: nil
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
expect(Ad.field_specs['allow_list'].default).to eq("---\n- 2\n")
|
862
|
+
expect(Ad.field_specs['allow_string'].default).to eq("---\n- abc\n")
|
863
|
+
expect(Ad.field_specs['allow_empty'].default).to eq(nil)
|
864
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
describe 'Hash' do
|
869
|
+
it 'allows serialize: Hash' do
|
870
|
+
class Ad < ActiveRecord::Base
|
871
|
+
fields do
|
872
|
+
allow_list :string, limit: 255, serialize: Hash, null: true
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
expect(Ad.serialize_args).to eq([[:allow_list, Hash]])
|
877
|
+
end
|
878
|
+
|
879
|
+
it 'allows Hash defaults' do
|
880
|
+
class Ad < ActiveRecord::Base
|
881
|
+
fields do
|
882
|
+
allow_loc :string, limit: 255, serialize: Hash, null: true, default: { 'state' => 'CA' }
|
883
|
+
allow_hash :string, limit: 255, serialize: Hash, null: true, default: {}
|
884
|
+
allow_null :string, limit: 255, serialize: Hash, null: true, default: nil
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
expect(Ad.field_specs['allow_loc'].default).to eq("---\nstate: CA\n")
|
889
|
+
expect(Ad.field_specs['allow_hash'].default).to eq(nil)
|
890
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
describe 'JSON' do
|
895
|
+
it 'allows serialize: JSON' do
|
896
|
+
class Ad < ActiveRecord::Base
|
897
|
+
fields do
|
898
|
+
allow_list :string, limit: 255, serialize: JSON
|
899
|
+
end
|
900
|
+
end
|
901
|
+
|
902
|
+
expect(Ad.serialize_args).to eq([[:allow_list, JSON]])
|
903
|
+
end
|
904
|
+
|
905
|
+
it 'allows JSON defaults' do
|
906
|
+
class Ad < ActiveRecord::Base
|
907
|
+
fields do
|
908
|
+
allow_hash :string, limit: 255, serialize: JSON, null: true, default: { 'state' => 'CA' }
|
909
|
+
allow_empty_array :string, limit: 255, serialize: JSON, null: true, default: []
|
910
|
+
allow_empty_hash :string, limit: 255, serialize: JSON, null: true, default: {}
|
911
|
+
allow_null :string, limit: 255, serialize: JSON, null: true, default: nil
|
912
|
+
end
|
913
|
+
end
|
914
|
+
|
915
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("{\"state\":\"CA\"}")
|
916
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq("[]")
|
917
|
+
expect(Ad.field_specs['allow_empty_hash'].default).to eq("{}")
|
918
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
class ValueClass
|
923
|
+
delegate :present?, :inspect, to: :@value
|
924
|
+
|
925
|
+
def initialize(value)
|
926
|
+
@value = value
|
927
|
+
end
|
928
|
+
|
929
|
+
class << self
|
930
|
+
def dump(object)
|
931
|
+
if object&.present?
|
932
|
+
object.inspect
|
933
|
+
end
|
934
|
+
end
|
935
|
+
|
936
|
+
def load(serialized)
|
937
|
+
if serialized
|
938
|
+
raise 'not used ???'
|
939
|
+
end
|
940
|
+
end
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
describe 'custom coder' do
|
945
|
+
it 'allows serialize: ValueClass' do
|
946
|
+
class Ad < ActiveRecord::Base
|
947
|
+
fields do
|
948
|
+
allow_list :string, limit: 255, serialize: ValueClass
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
expect(Ad.serialize_args).to eq([[:allow_list, ValueClass]])
|
953
|
+
end
|
954
|
+
|
955
|
+
it 'allows ValueClass defaults' do
|
956
|
+
class Ad < ActiveRecord::Base
|
957
|
+
fields do
|
958
|
+
allow_hash :string, limit: 255, serialize: ValueClass, null: true, default: ValueClass.new([2])
|
959
|
+
allow_empty_array :string, limit: 255, serialize: ValueClass, null: true, default: ValueClass.new([])
|
960
|
+
allow_null :string, limit: 255, serialize: ValueClass, null: true, default: nil
|
961
|
+
end
|
962
|
+
end
|
963
|
+
|
964
|
+
expect(Ad.field_specs['allow_hash'].default).to eq("[2]")
|
965
|
+
expect(Ad.field_specs['allow_empty_array'].default).to eq(nil)
|
966
|
+
expect(Ad.field_specs['allow_null'].default).to eq(nil)
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
it 'disallows serialize: with a non-string column type' do
|
971
|
+
expect do
|
972
|
+
class Ad < ActiveRecord::Base
|
973
|
+
fields do
|
974
|
+
allow_list :integer, limit: 8, serialize: true
|
975
|
+
end
|
976
|
+
end
|
977
|
+
end.to raise_exception(ArgumentError, /must be :string or :text/)
|
978
|
+
end
|
979
|
+
end
|
980
|
+
|
800
981
|
context "for Rails #{Rails::VERSION::MAJOR}" do
|
801
982
|
if Rails::VERSION::MAJOR >= 5
|
802
983
|
let(:optional_true) { { optional: true } }
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../../../lib/declare_schema/model/index_definition'
|
4
|
+
|
5
|
+
# Beware: It looks out that Rails' sqlite3 driver has a bug in retrieving indexes.
|
6
|
+
# In sqlite3/schema_statements, it skips over any index that starts with sqlite_:
|
7
|
+
# next if row["name"].starts_with?("sqlite_")
|
8
|
+
# but this will skip over any indexes created to support "unique" column constraints.
|
9
|
+
# This gem provides an explicit name for all indexes so it shouldn't be affected by the bug...
|
10
|
+
# unless you manually create any Sqlite tables with UNIQUE constraints.
|
11
|
+
|
12
|
+
RSpec.describe DeclareSchema::Model::IndexDefinition do
|
13
|
+
before do
|
14
|
+
load File.expand_path('../prepare_testapp.rb', __dir__)
|
15
|
+
|
16
|
+
class IndexSettingsTestModel < ActiveRecord::Base
|
17
|
+
fields do
|
18
|
+
name :string, limit: 127, index: true
|
19
|
+
|
20
|
+
timestamps
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:model_class) { IndexSettingsTestModel }
|
26
|
+
|
27
|
+
describe 'instance methods' do
|
28
|
+
let(:model) { model_class.new }
|
29
|
+
subject { declared_class.new(model_class) }
|
30
|
+
|
31
|
+
it 'has index_definitions' do
|
32
|
+
expect(model_class.index_definitions).to be_kind_of(Array)
|
33
|
+
expect(model_class.index_definitions.map(&:name)).to eq(['on_name'])
|
34
|
+
expect([:name, :fields, :unique].map { |attr| model_class.index_definitions[0].send(attr)}).to eq(
|
35
|
+
['on_name', ['name'], false]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'has index_definitions_with_primary_key' do
|
40
|
+
expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
|
41
|
+
result = model_class.index_definitions_with_primary_key.sort_by(&:name)
|
42
|
+
expect(result.map(&:name)).to eq(['PRIMARY', 'on_name'])
|
43
|
+
expect([:name, :fields, :unique].map { |attr| result[0].send(attr)}).to eq(
|
44
|
+
['PRIMARY', ['id'], true]
|
45
|
+
)
|
46
|
+
expect([:name, :fields, :unique].map { |attr| result[1].send(attr)}).to eq(
|
47
|
+
['on_name', ['name'], false]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'class << self' do
|
53
|
+
context 'with a migrated database' do
|
54
|
+
before do
|
55
|
+
ActiveRecord::Base.connection.execute <<~EOS
|
56
|
+
CREATE TABLE index_settings_test_models (
|
57
|
+
id INTEGER NOT NULL PRIMARY KEY,
|
58
|
+
name TEXT NOT NULL
|
59
|
+
)
|
60
|
+
EOS
|
61
|
+
ActiveRecord::Base.connection.execute <<~EOS
|
62
|
+
CREATE UNIQUE INDEX index_settings_test_models_on_name ON index_settings_test_models(name)
|
63
|
+
EOS
|
64
|
+
ActiveRecord::Base.connection.schema_cache.clear!
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'for_model' do
|
68
|
+
subject { described_class.for_model(model_class) }
|
69
|
+
|
70
|
+
it 'returns the indexes for the model' do
|
71
|
+
expect(subject.size).to eq(2), subject.inspect
|
72
|
+
expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
|
73
|
+
['index_settings_test_models_on_name', ['name'], true]
|
74
|
+
)
|
75
|
+
expect([:name, :columns, :unique].map { |attr| subject[1].send(attr) }).to eq(
|
76
|
+
['PRIMARY', ['id'], true]
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
# TODO: fill out remaining tests
|
83
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: declare_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Invoca Development adapted from hobo_fields by Tom Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-11-
|
11
|
+
date: 2020-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -59,7 +59,8 @@ files:
|
|
59
59
|
- lib/declare_schema/field_declaration_dsl.rb
|
60
60
|
- lib/declare_schema/model.rb
|
61
61
|
- lib/declare_schema/model/field_spec.rb
|
62
|
-
- lib/declare_schema/model/
|
62
|
+
- lib/declare_schema/model/foreign_key_definition.rb
|
63
|
+
- lib/declare_schema/model/index_definition.rb
|
63
64
|
- lib/declare_schema/railtie.rb
|
64
65
|
- lib/declare_schema/version.rb
|
65
66
|
- lib/generators/declare_schema/migration/USAGE
|
@@ -76,6 +77,7 @@ files:
|
|
76
77
|
- spec/lib/declare_schema/generator_spec.rb
|
77
78
|
- spec/lib/declare_schema/interactive_primary_key_spec.rb
|
78
79
|
- spec/lib/declare_schema/migration_generator_spec.rb
|
80
|
+
- spec/lib/declare_schema/model/index_definition_spec.rb
|
79
81
|
- spec/lib/declare_schema/prepare_testapp.rb
|
80
82
|
- spec/lib/generators/declare_schema/migration/migrator_spec.rb
|
81
83
|
- spec/spec_helper.rb
|
@@ -1,175 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeclareSchema
|
4
|
-
module Model
|
5
|
-
class IndexSpec
|
6
|
-
include Comparable
|
7
|
-
|
8
|
-
attr_reader :table, :fields, :explicit_name, :name, :unique, :where
|
9
|
-
|
10
|
-
class IndexNameTooLongError < RuntimeError; end
|
11
|
-
|
12
|
-
PRIMARY_KEY_NAME = "PRIMARY_KEY"
|
13
|
-
MYSQL_INDEX_NAME_MAX_LENGTH = 64
|
14
|
-
|
15
|
-
def initialize(model, fields, options = {})
|
16
|
-
@model = model
|
17
|
-
@table = options.delete(:table_name) || model.table_name
|
18
|
-
@fields = Array.wrap(fields).map(&:to_s)
|
19
|
-
@explicit_name = options[:name] unless options.delete(:allow_equivalent)
|
20
|
-
@name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
|
21
|
-
@unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
|
22
|
-
|
23
|
-
if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
|
24
|
-
raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
|
25
|
-
end
|
26
|
-
|
27
|
-
if (where = options[:where])
|
28
|
-
@where = where.start_with?('(') ? where : "(#{where})"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
class << self
|
33
|
-
# extract IndexSpecs from an existing table
|
34
|
-
def for_model(model, old_table_name = nil)
|
35
|
-
t = old_table_name || model.table_name
|
36
|
-
connection = model.connection.dup
|
37
|
-
# TODO: Change below to use prepend
|
38
|
-
class << connection # defeat Rails code that skips the primary keys by changing their name to PRIMARY_KEY_NAME
|
39
|
-
def each_hash(result)
|
40
|
-
super do |hash|
|
41
|
-
if hash[:Key_name] == "PRIMARY"
|
42
|
-
hash[:Key_name] = PRIMARY_KEY_NAME
|
43
|
-
end
|
44
|
-
yield hash
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
connection.indexes(t).map do |i|
|
49
|
-
new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name) unless model.ignore_indexes.include?(i.name)
|
50
|
-
end.compact
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def primary_key?
|
55
|
-
name == PRIMARY_KEY_NAME
|
56
|
-
end
|
57
|
-
|
58
|
-
def to_add_statement(new_table_name, existing_primary_key = nil)
|
59
|
-
if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
60
|
-
to_add_primary_key_statement(new_table_name, existing_primary_key)
|
61
|
-
else
|
62
|
-
r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
|
63
|
-
r += ", unique: true" if unique
|
64
|
-
r += ", where: '#{where}'" if where.present?
|
65
|
-
r += ", name: '#{name}'"
|
66
|
-
r
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def to_add_primary_key_statement(new_table_name, existing_primary_key)
|
71
|
-
drop = "DROP PRIMARY KEY, " if existing_primary_key
|
72
|
-
statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
|
73
|
-
"execute #{statement.inspect}"
|
74
|
-
end
|
75
|
-
|
76
|
-
def to_key
|
77
|
-
@key ||= [table, fields, name, unique, where].map(&:to_s)
|
78
|
-
end
|
79
|
-
|
80
|
-
def settings
|
81
|
-
@settings ||= [table, fields, unique].map(&:to_s)
|
82
|
-
end
|
83
|
-
|
84
|
-
def hash
|
85
|
-
to_key.hash
|
86
|
-
end
|
87
|
-
|
88
|
-
def <=>(rhs)
|
89
|
-
to_key <=> rhs.to_key
|
90
|
-
end
|
91
|
-
|
92
|
-
def equivalent?(rhs)
|
93
|
-
settings == rhs.settings
|
94
|
-
end
|
95
|
-
|
96
|
-
def with_name(new_name)
|
97
|
-
self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
|
98
|
-
end
|
99
|
-
|
100
|
-
alias eql? ==
|
101
|
-
end
|
102
|
-
|
103
|
-
class ForeignKeySpec
|
104
|
-
include Comparable
|
105
|
-
|
106
|
-
attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
|
107
|
-
|
108
|
-
def initialize(model, foreign_key, options = {})
|
109
|
-
@model = model
|
110
|
-
@foreign_key = foreign_key.presence
|
111
|
-
@options = options
|
112
|
-
|
113
|
-
@child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
|
114
|
-
@parent_table_name = options[:parent_table]
|
115
|
-
@foreign_key_name = options[:foreign_key] || self.foreign_key
|
116
|
-
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
|
117
|
-
@constraint_name = options[:constraint_name] || @index_name || ''
|
118
|
-
@on_delete_cascade = options[:dependent] == :delete
|
119
|
-
|
120
|
-
# Empty constraint lets mysql generate the name
|
121
|
-
end
|
122
|
-
|
123
|
-
class << self
|
124
|
-
def for_model(model, old_table_name)
|
125
|
-
show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
|
126
|
-
constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
|
127
|
-
|
128
|
-
constraints.map do |fkc|
|
129
|
-
options = {}
|
130
|
-
name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
|
131
|
-
options[:constraint_name] = name
|
132
|
-
options[:parent_table] = parent_table
|
133
|
-
options[:foreign_key] = foreign_key
|
134
|
-
options[:dependent] = :delete if fkc['ON DELETE CASCADE']
|
135
|
-
|
136
|
-
new(model, foreign_key, options)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def parent_table_name
|
142
|
-
@parent_table_name ||=
|
143
|
-
options[:class_name]&.is_a?(Class) &&
|
144
|
-
options[:class_name].respond_to?(:table_name) &&
|
145
|
-
options[:class_name]&.table_name
|
146
|
-
@parent_table_name ||=
|
147
|
-
options[:class_name]&.constantize &&
|
148
|
-
options[:class_name].constantize.respond_to?(:table_name) &&
|
149
|
-
options[:class_name].constantize.table_name ||
|
150
|
-
foreign_key.gsub(/_id/, '').camelize.constantize.table_name
|
151
|
-
end
|
152
|
-
|
153
|
-
attr_writer :parent_table_name
|
154
|
-
|
155
|
-
def to_add_statement(_ = true)
|
156
|
-
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}"
|
157
|
-
"execute #{statement.inspect}"
|
158
|
-
end
|
159
|
-
|
160
|
-
def to_key
|
161
|
-
@key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
|
162
|
-
end
|
163
|
-
|
164
|
-
def hash
|
165
|
-
to_key.hash
|
166
|
-
end
|
167
|
-
|
168
|
-
def <=>(rhs)
|
169
|
-
to_key <=> rhs.to_key
|
170
|
-
end
|
171
|
-
|
172
|
-
alias eql? ==
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|