declare_schema 0.3.1 → 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 +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
|