declare_schema 0.3.0 → 0.5.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|