declare_schema 0.3.0 → 0.5.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +14 -0
- data/CHANGELOG.md +36 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +16 -14
- data/README.md +66 -0
- data/gemfiles/rails_4.gemfile +1 -0
- data/gemfiles/rails_5.gemfile +1 -0
- data/gemfiles/rails_6.gemfile +1 -0
- data/lib/declare_schema.rb +3 -1
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +2 -1
- data/lib/declare_schema/model.rb +59 -22
- data/lib/declare_schema/model/field_spec.rb +82 -26
- data/lib/declare_schema/model/foreign_key_definition.rb +73 -0
- data/lib/declare_schema/model/index_definition.rb +138 -0
- data/lib/declare_schema/model/table_options_definition.rb +83 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +105 -52
- data/spec/lib/declare_schema/api_spec.rb +7 -8
- data/spec/lib/declare_schema/generator_spec.rb +51 -10
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +16 -14
- data/spec/lib/declare_schema/migration_generator_spec.rb +514 -213
- data/spec/lib/declare_schema/model/index_definition_spec.rb +123 -0
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +84 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +28 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/acceptance_spec_helpers.rb +57 -0
- metadata +12 -7
- data/.dependabot/config.yml +0 -10
- data/lib/declare_schema/model/index_spec.rb +0 -175
@@ -54,6 +54,9 @@ module DeclareSchema
|
|
54
54
|
end
|
55
55
|
when :string
|
56
56
|
@options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
|
57
|
+
else
|
58
|
+
@options[:collation] and raise "collation may only given for :string and :text fields"
|
59
|
+
@options[:charset] and raise "charset may only given for :string and :text fields"
|
57
60
|
end
|
58
61
|
@position = position_option || model.field_specs.length
|
59
62
|
end
|
@@ -102,6 +105,18 @@ module DeclareSchema
|
|
102
105
|
@options[:default]
|
103
106
|
end
|
104
107
|
|
108
|
+
def collation
|
109
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
110
|
+
(@options[:collation] || model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation).to_s
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def charset
|
115
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
116
|
+
(@options[:charset] || model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset).to_s
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
105
120
|
def same_type?(col_spec)
|
106
121
|
type = sql_type
|
107
122
|
normalized_type = TYPE_SYNONYMS[type] || type
|
@@ -109,36 +124,77 @@ module DeclareSchema
|
|
109
124
|
normalized_type == normalized_col_spec_type
|
110
125
|
end
|
111
126
|
|
112
|
-
def different_to?(col_spec)
|
113
|
-
!
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
127
|
+
def different_to?(table_name, col_spec)
|
128
|
+
!same_as(table_name, col_spec)
|
129
|
+
end
|
130
|
+
|
131
|
+
def same_as(table_name, col_spec)
|
132
|
+
same_type?(col_spec) &&
|
133
|
+
same_attributes?(col_spec) &&
|
134
|
+
(!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def same_attributes?(col_spec)
|
140
|
+
native_type = native_types[type]
|
141
|
+
check_attributes = [:null, :default]
|
142
|
+
check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
|
143
|
+
check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
|
144
|
+
check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
|
145
|
+
(sql_type == :text && self.class.mysql_text_limits?)
|
146
|
+
check_attributes.all? do |k|
|
147
|
+
if k == :default
|
148
|
+
case Rails::VERSION::MAJOR
|
149
|
+
when 4
|
150
|
+
col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
|
151
|
+
else
|
152
|
+
cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
|
153
|
+
cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
col_value = col_spec.send(k)
|
157
|
+
if col_value.nil? && native_type
|
158
|
+
col_value = native_type[k]
|
137
159
|
end
|
160
|
+
col_value == send(k)
|
138
161
|
end
|
162
|
+
end
|
139
163
|
end
|
140
164
|
|
141
|
-
|
165
|
+
def same_charset_and_collation?(table_name, col_spec)
|
166
|
+
current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
|
167
|
+
|
168
|
+
collation == current_collation_and_charset[:collation] &&
|
169
|
+
charset == current_collation_and_charset[:charset]
|
170
|
+
end
|
171
|
+
|
172
|
+
def collation_and_charset_for_column(table_name, col_spec)
|
173
|
+
column_name = col_spec.name
|
174
|
+
connection = ActiveRecord::Base.connection
|
175
|
+
|
176
|
+
if connection.class.name.match?(/mysql/i)
|
177
|
+
database_name = connection.current_database
|
178
|
+
|
179
|
+
defaults = connection.select_one(<<~EOS)
|
180
|
+
SELECT C.character_set_name, C.collation_name
|
181
|
+
FROM information_schema.`COLUMNS` C
|
182
|
+
WHERE C.table_schema = #{connection.quote_string(database_name)} AND
|
183
|
+
C.table_name = #{connection.quote_string(table_name)} AND
|
184
|
+
C.column_name = #{connection.quote_string(column_name)};
|
185
|
+
EOS
|
186
|
+
|
187
|
+
defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
|
188
|
+
defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
|
189
|
+
|
190
|
+
{
|
191
|
+
charset: defaults["character_set_name"],
|
192
|
+
collation: defaults["collation_name"]
|
193
|
+
}
|
194
|
+
else
|
195
|
+
{}
|
196
|
+
end
|
197
|
+
end
|
142
198
|
|
143
199
|
def native_type?(type)
|
144
200
|
type.to_sym != :primary_key && native_types.has_key?(type)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
module Model
|
5
|
+
class ForeignKeyDefinition
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
|
9
|
+
|
10
|
+
def initialize(model, foreign_key, options = {})
|
11
|
+
@model = model
|
12
|
+
@foreign_key = foreign_key.presence
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
@child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
|
16
|
+
@parent_table_name = options[:parent_table]
|
17
|
+
@foreign_key_name = options[:foreign_key] || self.foreign_key
|
18
|
+
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
|
19
|
+
@constraint_name = options[:constraint_name] || @index_name || ''
|
20
|
+
@on_delete_cascade = options[:dependent] == :delete
|
21
|
+
|
22
|
+
# Empty constraint lets mysql generate the name
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def for_model(model, old_table_name)
|
27
|
+
show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
|
28
|
+
constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
|
29
|
+
|
30
|
+
constraints.map do |fkc|
|
31
|
+
options = {}
|
32
|
+
name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
|
33
|
+
options[:constraint_name] = name
|
34
|
+
options[:parent_table] = parent_table
|
35
|
+
options[:foreign_key] = foreign_key
|
36
|
+
options[:dependent] = :delete if fkc['ON DELETE CASCADE']
|
37
|
+
|
38
|
+
new(model, foreign_key, options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent_table_name
|
44
|
+
@parent_table_name ||=
|
45
|
+
if (klass = options[:class_name])
|
46
|
+
klass = klass.to_s.constantize unless klass.is_a?(Class)
|
47
|
+
klass.try(:table_name)
|
48
|
+
end || foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_writer :parent_table_name
|
52
|
+
|
53
|
+
def to_add_statement
|
54
|
+
statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
|
55
|
+
"execute #{statement.inspect}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def key
|
59
|
+
@key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def hash
|
63
|
+
key.hash
|
64
|
+
end
|
65
|
+
|
66
|
+
def <=>(rhs)
|
67
|
+
key <=> rhs.key
|
68
|
+
end
|
69
|
+
|
70
|
+
alias eql? ==
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
module Model
|
5
|
+
class IndexDefinition
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
# TODO: replace `fields` with `columns` and remove alias. -Colin
|
9
|
+
attr_reader :table, :fields, :explicit_name, :name, :unique, :where
|
10
|
+
alias columns fields
|
11
|
+
|
12
|
+
class IndexNameTooLongError < RuntimeError; end
|
13
|
+
|
14
|
+
PRIMARY_KEY_NAME = "PRIMARY"
|
15
|
+
MYSQL_INDEX_NAME_MAX_LENGTH = 64
|
16
|
+
|
17
|
+
def initialize(model, fields, options = {})
|
18
|
+
@model = model
|
19
|
+
@table = options.delete(:table_name) || model.table_name
|
20
|
+
@fields = Array.wrap(fields).map(&:to_s)
|
21
|
+
@explicit_name = options[:name] unless options.delete(:allow_equivalent)
|
22
|
+
@name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
|
23
|
+
@unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
|
24
|
+
|
25
|
+
if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
|
26
|
+
raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
|
27
|
+
end
|
28
|
+
|
29
|
+
if (where = options[:where])
|
30
|
+
@where = where.start_with?('(') ? where : "(#{where})"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
# extract IndexSpecs from an existing table
|
36
|
+
# always includes the PRIMARY KEY index
|
37
|
+
def for_model(model, old_table_name = nil)
|
38
|
+
t = old_table_name || model.table_name
|
39
|
+
|
40
|
+
primary_key_columns = Array(model.connection.primary_key(t)).presence || sqlite_compound_primary_key(model, t) or
|
41
|
+
raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
|
42
|
+
|
43
|
+
primary_key_found = false
|
44
|
+
index_definitions = model.connection.indexes(t).map do |i|
|
45
|
+
model.ignore_indexes.include?(i.name) and next
|
46
|
+
if i.name == PRIMARY_KEY_NAME
|
47
|
+
i.columns == primary_key_columns && i.unique or
|
48
|
+
raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
|
49
|
+
primary_key_found = true
|
50
|
+
elsif i.columns == primary_key_columns && i.unique
|
51
|
+
# skip this primary key index since we'll create it below, with PRIMARY_KEY_NAME
|
52
|
+
next
|
53
|
+
end
|
54
|
+
new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
|
55
|
+
end.compact
|
56
|
+
|
57
|
+
if !primary_key_found
|
58
|
+
index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
|
59
|
+
end
|
60
|
+
index_definitions
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# This is the old approach which is still needed for MySQL in Rails 4 and SQLite
|
66
|
+
def sqlite_compound_primary_key(model, table)
|
67
|
+
ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) || Rails::VERSION::MAJOR < 5 or return nil
|
68
|
+
|
69
|
+
connection = model.connection.dup
|
70
|
+
|
71
|
+
class << connection # defeat Rails MySQL driver code that skips the primary key by changing its name to a symbol
|
72
|
+
def each_hash(result)
|
73
|
+
super do |hash|
|
74
|
+
if hash[:Key_name] == PRIMARY_KEY_NAME
|
75
|
+
hash[:Key_name] = PRIMARY_KEY_NAME.to_sym
|
76
|
+
end
|
77
|
+
yield hash
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
pk_index = connection.indexes(table).find { |index| index.name.to_s == PRIMARY_KEY_NAME } or return nil
|
83
|
+
|
84
|
+
Array(pk_index.columns)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def primary_key?
|
89
|
+
name == PRIMARY_KEY_NAME
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_add_statement(new_table_name, existing_primary_key = nil)
|
93
|
+
if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
94
|
+
to_add_primary_key_statement(new_table_name, existing_primary_key)
|
95
|
+
else
|
96
|
+
# Note: + below keeps that interpolated string from being frozen, so we can << into it.
|
97
|
+
r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
|
98
|
+
r << ", unique: true" if unique
|
99
|
+
r << ", where: '#{where}'" if where.present?
|
100
|
+
r << ", name: '#{name}'"
|
101
|
+
r
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_add_primary_key_statement(new_table_name, existing_primary_key)
|
106
|
+
drop = "DROP PRIMARY KEY, " if existing_primary_key
|
107
|
+
statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
|
108
|
+
"execute #{statement.inspect}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_key
|
112
|
+
@key ||= [table, fields, name, unique, where].map(&:to_s)
|
113
|
+
end
|
114
|
+
|
115
|
+
def settings
|
116
|
+
@settings ||= [table, fields, unique].map(&:to_s)
|
117
|
+
end
|
118
|
+
|
119
|
+
def hash
|
120
|
+
to_key.hash
|
121
|
+
end
|
122
|
+
|
123
|
+
def <=>(rhs)
|
124
|
+
to_key <=> rhs.to_key
|
125
|
+
end
|
126
|
+
|
127
|
+
def equivalent?(rhs)
|
128
|
+
settings == rhs.settings
|
129
|
+
end
|
130
|
+
|
131
|
+
def with_name(new_name)
|
132
|
+
self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
|
133
|
+
end
|
134
|
+
|
135
|
+
alias eql? ==
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
module Model
|
5
|
+
class TableOptionsDefinition
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
TABLE_OPTIONS_TO_SQL_MAPPINGS = {
|
9
|
+
charset: 'CHARACTER SET',
|
10
|
+
collation: 'COLLATE'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def for_model(model, old_table_name = nil)
|
15
|
+
table_name = old_table_name || model.table_name
|
16
|
+
table_options = if model.connection.class.name.match?(/mysql/i)
|
17
|
+
mysql_table_options(model.connection, table_name)
|
18
|
+
else
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
|
22
|
+
new(table_name, table_options)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def mysql_table_options(connection, table_name)
|
28
|
+
database = connection.current_database
|
29
|
+
defaults = connection.select_one(<<~EOS)
|
30
|
+
SELECT CCSA.character_set_name, CCSA.collation_name
|
31
|
+
FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
|
32
|
+
WHERE CCSA.collation_name = T.table_collation AND
|
33
|
+
T.table_schema = #{connection.quote_string(database)} AND
|
34
|
+
T.table_name = #{connection.quote_string(table_name)};
|
35
|
+
EOS
|
36
|
+
|
37
|
+
defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
|
38
|
+
defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
|
39
|
+
|
40
|
+
{
|
41
|
+
charset: defaults["character_set_name"],
|
42
|
+
collation: defaults["collation_name"]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :table_name, :table_options
|
48
|
+
|
49
|
+
def initialize(table_name, table_options = {})
|
50
|
+
@table_name = table_name
|
51
|
+
@table_options = table_options
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_key
|
55
|
+
@key ||= [table_name, table_options].map(&:to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
def settings
|
59
|
+
@settings ||= table_options.map { |name, value| "#{TABLE_OPTIONS_TO_SQL_MAPPINGS[name]} #{value}" if value }.compact.join(" ")
|
60
|
+
end
|
61
|
+
|
62
|
+
def hash
|
63
|
+
to_key.hash
|
64
|
+
end
|
65
|
+
|
66
|
+
def <=>(rhs)
|
67
|
+
to_key <=> rhs.to_key
|
68
|
+
end
|
69
|
+
|
70
|
+
def equivalent?(rhs)
|
71
|
+
settings == rhs.settings
|
72
|
+
end
|
73
|
+
|
74
|
+
alias eql? ==
|
75
|
+
alias to_s settings
|
76
|
+
|
77
|
+
def alter_table_statement
|
78
|
+
statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{to_s};"
|
79
|
+
"execute #{statement.inspect}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
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
|
@@ -69,13 +69,19 @@ module Generators
|
|
69
69
|
class Migrator
|
70
70
|
class Error < RuntimeError; end
|
71
71
|
|
72
|
-
|
73
|
-
|
72
|
+
DEFAULT_CHARSET = :utf8mb4
|
73
|
+
DEFAULT_COLLATION = :utf8mb4_general
|
74
|
+
|
75
|
+
@ignore_models = []
|
76
|
+
@ignore_tables = []
|
74
77
|
@before_generating_migration_callback = nil
|
75
|
-
@active_record_class
|
78
|
+
@active_record_class = ActiveRecord::Base
|
79
|
+
@default_charset = DEFAULT_CHARSET
|
80
|
+
@default_collation = DEFAULT_COLLATION
|
76
81
|
|
77
82
|
class << self
|
78
|
-
attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
|
83
|
+
attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
|
84
|
+
:active_record_class, :default_charset, :default_collation
|
79
85
|
attr_reader :before_generating_migration_callback
|
80
86
|
|
81
87
|
def active_record_class
|
@@ -292,56 +298,80 @@ module Generators
|
|
292
298
|
"drop_table :#{t}"
|
293
299
|
end * "\n"
|
294
300
|
|
295
|
-
changes
|
296
|
-
undo_changes
|
297
|
-
index_changes
|
298
|
-
undo_index_changes
|
299
|
-
fk_changes
|
300
|
-
undo_fk_changes
|
301
|
+
changes = []
|
302
|
+
undo_changes = []
|
303
|
+
index_changes = []
|
304
|
+
undo_index_changes = []
|
305
|
+
fk_changes = []
|
306
|
+
undo_fk_changes = []
|
307
|
+
table_options_changes = []
|
308
|
+
undo_table_options_changes = []
|
309
|
+
|
301
310
|
to_change.each do |t|
|
302
311
|
model = models_by_table_name[t]
|
303
312
|
table = to_rename.key(t) || model.table_name
|
304
313
|
if table.in?(db_tables)
|
305
|
-
change, undo, index_change, undo_index, fk_change, undo_fk = change_table(model, table)
|
314
|
+
change, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
|
306
315
|
changes << change
|
307
316
|
undo_changes << undo
|
308
317
|
index_changes << index_change
|
309
318
|
undo_index_changes << undo_index
|
310
319
|
fk_changes << fk_change
|
311
320
|
undo_fk_changes << undo_fk
|
321
|
+
table_options_changes << table_options_change
|
322
|
+
undo_table_options_changes << undo_table_options_change
|
312
323
|
end
|
313
324
|
end
|
314
325
|
|
315
|
-
up = [renames, drops, creates, changes, index_changes, fk_changes].flatten.reject(&:blank?) * "\n\n"
|
316
|
-
down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
|
326
|
+
up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
|
327
|
+
down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes, undo_table_options_changes].flatten.reject(&:blank?) * "\n\n"
|
317
328
|
|
318
329
|
[up, down]
|
319
330
|
end
|
320
331
|
|
321
332
|
def create_table(model)
|
322
|
-
longest_field_name
|
323
|
-
disable_auto_increment
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
333
|
+
longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
|
334
|
+
disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
|
335
|
+
table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
|
336
|
+
field_definitions = [
|
337
|
+
disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
|
338
|
+
*(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
|
339
|
+
].compact
|
340
|
+
|
341
|
+
<<~EOS.strip
|
342
|
+
create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
|
343
|
+
#{field_definitions.join("\n")}
|
331
344
|
end
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
345
|
+
|
346
|
+
#{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
|
347
|
+
#{create_indexes(model).join("\n") unless Migrator.disable_indexing}
|
348
|
+
#{create_constraints(model).join("\n") unless Migrator.disable_indexing}
|
349
|
+
EOS
|
350
|
+
end
|
351
|
+
|
352
|
+
def create_table_options(model, disable_auto_increment)
|
353
|
+
if model.primary_key.blank? || disable_auto_increment
|
354
|
+
"id: false"
|
355
|
+
elsif model.primary_key == "id"
|
356
|
+
"id: :bigint"
|
357
|
+
else
|
358
|
+
"primary_key: :#{model.primary_key}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def table_options_for_model(model)
|
363
|
+
if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
364
|
+
{}
|
365
|
+
else
|
366
|
+
{
|
367
|
+
charset: model.table_options[:charset] || Migrator.default_charset,
|
368
|
+
collation: model.table_options[:collation] || Migrator.default_collation
|
369
|
+
}
|
370
|
+
end
|
341
371
|
end
|
342
372
|
|
343
373
|
def create_indexes(model)
|
344
|
-
model.
|
374
|
+
model.index_definitions.map { |i| i.to_add_statement(model.table_name) }
|
345
375
|
end
|
346
376
|
|
347
377
|
def create_constraints(model)
|
@@ -351,7 +381,7 @@ module Generators
|
|
351
381
|
def create_field(field_spec, field_name_width)
|
352
382
|
options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
|
353
383
|
args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
|
354
|
-
format("
|
384
|
+
format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
|
355
385
|
end
|
356
386
|
|
357
387
|
def change_table(model, current_table_name)
|
@@ -413,15 +443,17 @@ module Generators
|
|
413
443
|
col_name = old_names[c] || c
|
414
444
|
col = db_columns[col_name]
|
415
445
|
spec = model.field_specs[c]
|
416
|
-
if spec.different_to?(col) # TODO: DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
|
446
|
+
if spec.different_to?(current_table_name, col) # TODO: TECH-4814 DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
|
417
447
|
change_spec = fk_field_options(model, c)
|
418
448
|
change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
|
419
449
|
::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
|
420
450
|
(spec.limit || col.limit)
|
421
|
-
change_spec[:precision]
|
422
|
-
change_spec[:scale]
|
423
|
-
change_spec[:null]
|
424
|
-
change_spec[:default]
|
451
|
+
change_spec[:precision] = spec.precision unless spec.precision.nil?
|
452
|
+
change_spec[:scale] = spec.scale unless spec.scale.nil?
|
453
|
+
change_spec[:null] = spec.null unless spec.null && col.null
|
454
|
+
change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
|
455
|
+
change_spec[:collation] = spec.collation unless spec.collation.nil?
|
456
|
+
change_spec[:charset] = spec.charset unless spec.charset.nil?
|
425
457
|
|
426
458
|
changes << "change_column :#{new_table_name}, :#{c}, " +
|
427
459
|
([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
|
@@ -436,21 +468,28 @@ module Generators
|
|
436
468
|
else
|
437
469
|
change_foreign_key_constraints(model, current_table_name)
|
438
470
|
end
|
471
|
+
table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
472
|
+
change_table_options(model, current_table_name)
|
473
|
+
else
|
474
|
+
[[], []]
|
475
|
+
end
|
439
476
|
|
440
477
|
[(renames + adds + removes + changes) * "\n",
|
441
478
|
(undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
|
442
479
|
index_changes * "\n",
|
443
480
|
undo_index_changes * "\n",
|
444
481
|
fk_changes * "\n",
|
445
|
-
undo_fk_changes * "\n"
|
482
|
+
undo_fk_changes * "\n",
|
483
|
+
table_options_changes * "\n",
|
484
|
+
undo_table_options_changes * "\n"]
|
446
485
|
end
|
447
486
|
|
448
487
|
def change_indexes(model, old_table_name)
|
449
488
|
return [[], []] if Migrator.disable_constraints
|
450
489
|
|
451
490
|
new_table_name = model.table_name
|
452
|
-
existing_indexes = ::DeclareSchema::Model::
|
453
|
-
model_indexes_with_equivalents = model.
|
491
|
+
existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
|
492
|
+
model_indexes_with_equivalents = model.index_definitions_with_primary_key
|
454
493
|
model_indexes = model_indexes_with_equivalents.map do |i|
|
455
494
|
if i.explicit_name.nil?
|
456
495
|
if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
|
@@ -458,20 +497,20 @@ module Generators
|
|
458
497
|
end
|
459
498
|
end || i
|
460
499
|
end
|
461
|
-
existing_has_primary_key = existing_indexes.any? { |i| i.name ==
|
462
|
-
model_has_primary_key = model_indexes.any? { |i| i.name ==
|
500
|
+
existing_has_primary_key = existing_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
501
|
+
model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
463
502
|
|
464
503
|
add_indexes_init = model_indexes - existing_indexes
|
465
504
|
drop_indexes_init = existing_indexes - model_indexes
|
466
505
|
undo_add_indexes = []
|
467
506
|
undo_drop_indexes = []
|
468
507
|
add_indexes = add_indexes_init.map do |i|
|
469
|
-
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name ==
|
508
|
+
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
470
509
|
i.to_add_statement(new_table_name, existing_has_primary_key)
|
471
510
|
end
|
472
511
|
drop_indexes = drop_indexes_init.map do |i|
|
473
512
|
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 ==
|
513
|
+
drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
475
514
|
end.compact
|
476
515
|
|
477
516
|
# the order is important here - adding a :unique, for instance needs to remove then add
|
@@ -489,7 +528,7 @@ module Generators
|
|
489
528
|
return [[], []] if Migrator.disable_indexing
|
490
529
|
|
491
530
|
new_table_name = model.table_name
|
492
|
-
existing_fks = ::DeclareSchema::Model::
|
531
|
+
existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
|
493
532
|
model_fks = model.constraint_specs
|
494
533
|
add_fks = model_fks - existing_fks
|
495
534
|
drop_fks = existing_fks - model_fks
|
@@ -552,6 +591,20 @@ module Generators
|
|
552
591
|
end
|
553
592
|
end
|
554
593
|
|
594
|
+
def change_table_options(model, current_table_name)
|
595
|
+
old_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.for_model(model, current_table_name)
|
596
|
+
new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
|
597
|
+
|
598
|
+
if old_options_definition.equivalent?(new_options_definition)
|
599
|
+
[[], []]
|
600
|
+
else
|
601
|
+
[
|
602
|
+
[new_options_definition.alter_table_statement],
|
603
|
+
[old_options_definition.alter_table_statement]
|
604
|
+
]
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
555
608
|
def revert_table(table)
|
556
609
|
res = StringIO.new
|
557
610
|
schema_dumper_klass = case Rails::VERSION::MAJOR
|