declare_schema 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +58 -54
- data/lib/declare_schema/field_declaration_dsl.rb +3 -4
- data/lib/declare_schema/model.rb +1 -2
- data/lib/declare_schema/model/column.rb +167 -0
- data/lib/declare_schema/model/field_spec.rb +59 -143
- data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +111 -133
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
- data/spec/lib/declare_schema/field_spec_spec.rb +142 -38
- data/spec/lib/declare_schema/migration_generator_spec.rb +73 -69
- data/spec/lib/declare_schema/model/column_spec.rb +141 -0
- data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +2 -11
- metadata +5 -2
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'column'
|
4
|
+
|
3
5
|
module DeclareSchema
|
6
|
+
class MysqlTextMayNotHaveDefault < RuntimeError; end
|
7
|
+
|
4
8
|
module Model
|
5
9
|
class FieldSpec
|
6
|
-
class UnknownSqlTypeError < RuntimeError; end
|
7
10
|
|
8
11
|
MYSQL_TINYTEXT_LIMIT = 0xff
|
9
12
|
MYSQL_TEXT_LIMIT = 0xffff
|
@@ -30,7 +33,18 @@ module DeclareSchema
|
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
33
|
-
attr_reader :model, :name, :type, :position, :options
|
36
|
+
attr_reader :model, :name, :type, :sql_type, :position, :options, :sql_options
|
37
|
+
|
38
|
+
TYPE_SYNONYMS = { timestamp: :datetime }.freeze # TODO: drop this synonym. -Colin
|
39
|
+
|
40
|
+
SQL_OPTIONS = [:limit, :precision, :scale, :null, :default, :charset, :collation].freeze
|
41
|
+
NON_SQL_OPTIONS = [:ruby_default, :validates].freeze
|
42
|
+
VALID_OPTIONS = (SQL_OPTIONS + NON_SQL_OPTIONS).freeze
|
43
|
+
OPTION_INDEXES = Hash[VALID_OPTIONS.each_with_index.to_a].freeze
|
44
|
+
|
45
|
+
VALID_OPTIONS.each do |option|
|
46
|
+
define_method(option) { @options[option] }
|
47
|
+
end
|
34
48
|
|
35
49
|
def initialize(model, name, type, position: 0, **options)
|
36
50
|
# TODO: TECH-5116
|
@@ -42,170 +56,72 @@ module DeclareSchema
|
|
42
56
|
@model = model
|
43
57
|
@name = name.to_sym
|
44
58
|
type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
|
45
|
-
@type = type
|
59
|
+
@type = TYPE_SYNONYMS[type] || type
|
46
60
|
@position = position
|
47
|
-
@options = options
|
61
|
+
@options = options.dup
|
62
|
+
|
63
|
+
@options.has_key?(:null) or @options[:null] = false
|
64
|
+
|
48
65
|
case type
|
49
66
|
when :text
|
50
|
-
@options[:default] and raise "default may not be given for :text field #{model}##{@name}"
|
51
67
|
if self.class.mysql_text_limits?
|
68
|
+
@options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
|
52
69
|
@options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
|
70
|
+
else
|
71
|
+
@options[:limit] = nil
|
53
72
|
end
|
54
73
|
when :string
|
55
|
-
@options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
|
74
|
+
@options[:limit] or raise "limit: must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
|
56
75
|
when :bigint
|
57
76
|
@type = :integer
|
58
|
-
@options =
|
77
|
+
@options[:limit] = 8
|
59
78
|
end
|
60
79
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
80
|
+
# TODO: Do we really need to support a :sql_type option? Ideally, drop it. -Colin
|
81
|
+
@sql_type = @options.delete(:sql_type) || Column.sql_type(@type)
|
82
|
+
|
83
|
+
if @sql_type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
|
84
|
+
@options[:limit] ||= Column.native_types[@sql_type][:limit]
|
66
85
|
else
|
67
|
-
@options
|
68
|
-
@options
|
86
|
+
@sql_type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@sql_type} in field #{model}##{@name}")
|
87
|
+
@options.delete(:limit)
|
69
88
|
end
|
70
|
-
end
|
71
89
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
90
|
+
if @sql_type == :decimal
|
91
|
+
@options[:precision] or warn("precision: required for :decimal type in field #{model}##{@name}")
|
92
|
+
@options[:scale] or warn("scale: required for :decimal type in field #{model}##{@name}")
|
93
|
+
else
|
94
|
+
if @sql_type != :datetime
|
95
|
+
@options.has_key?(:precision) and warn("precision: only allowed for :decimal type or :datetime for SQL type #{@sql_type} in field #{model}##{@name}")
|
96
|
+
end
|
97
|
+
@options.has_key?(:scale) and warn("scale: only allowed for :decimal type for SQL type #{@sql_type} in field #{model}##{@name}")
|
79
98
|
end
|
80
99
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
else
|
86
|
-
field_class = DeclareSchema.to_class(type)
|
87
|
-
field_class && field_class::COLUMN_TYPE or raise UnknownSqlTypeError, "#{type.inspect} for #{model}##{@name}"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def sql_options
|
93
|
-
@options.except(:ruby_default, :validates)
|
94
|
-
end
|
95
|
-
|
96
|
-
def limit
|
97
|
-
@options[:limit] || native_types[sql_type][:limit]
|
98
|
-
end
|
99
|
-
|
100
|
-
def precision
|
101
|
-
@options[:precision]
|
102
|
-
end
|
103
|
-
|
104
|
-
def scale
|
105
|
-
@options[:scale]
|
106
|
-
end
|
107
|
-
|
108
|
-
def null
|
109
|
-
!:null.in?(@options) || @options[:null]
|
110
|
-
end
|
111
|
-
|
112
|
-
def default
|
113
|
-
@options[:default]
|
114
|
-
end
|
115
|
-
|
116
|
-
def charset
|
117
|
-
@options[:charset]
|
118
|
-
end
|
119
|
-
|
120
|
-
def collation
|
121
|
-
@options[:collation]
|
122
|
-
end
|
123
|
-
|
124
|
-
def same_type?(col_spec)
|
125
|
-
type = sql_type
|
126
|
-
normalized_type = TYPE_SYNONYMS[type] || type
|
127
|
-
normalized_col_spec_type = TYPE_SYNONYMS[col_spec.type] || col_spec.type
|
128
|
-
normalized_type == normalized_col_spec_type
|
129
|
-
end
|
130
|
-
|
131
|
-
def different_to?(table_name, col_spec)
|
132
|
-
!same_as(table_name, col_spec)
|
133
|
-
end
|
134
|
-
|
135
|
-
def same_as(table_name, col_spec)
|
136
|
-
same_type?(col_spec) &&
|
137
|
-
same_attributes?(col_spec) &&
|
138
|
-
(!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def same_attributes?(col_spec)
|
144
|
-
native_type = native_types[type]
|
145
|
-
check_attributes = [:null, :default]
|
146
|
-
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
|
147
|
-
check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
|
148
|
-
check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
|
149
|
-
(sql_type == :text && self.class.mysql_text_limits?)
|
150
|
-
check_attributes.all? do |k|
|
151
|
-
if k == :default
|
152
|
-
case Rails::VERSION::MAJOR
|
153
|
-
when 4
|
154
|
-
col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
|
155
|
-
else
|
156
|
-
cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
|
157
|
-
cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
|
158
|
-
end
|
100
|
+
if @type.in?([:text, :string])
|
101
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
102
|
+
@options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
|
103
|
+
@options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
|
159
104
|
else
|
160
|
-
|
161
|
-
|
162
|
-
col_value = native_type[k]
|
163
|
-
end
|
164
|
-
col_value == send(k)
|
105
|
+
@options.delete(:charset)
|
106
|
+
@options.delete(:collation)
|
165
107
|
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def same_charset_and_collation?(table_name, col_spec)
|
170
|
-
current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
|
171
|
-
|
172
|
-
collation == current_collation_and_charset[:collation] &&
|
173
|
-
charset == current_collation_and_charset[:charset]
|
174
|
-
end
|
175
|
-
|
176
|
-
def collation_and_charset_for_column(table_name, col_spec)
|
177
|
-
column_name = col_spec.name
|
178
|
-
connection = ActiveRecord::Base.connection
|
179
|
-
|
180
|
-
if connection.class.name.match?(/mysql/i)
|
181
|
-
database_name = connection.current_database
|
182
|
-
|
183
|
-
defaults = connection.select_one(<<~EOS)
|
184
|
-
SELECT C.character_set_name, C.collation_name
|
185
|
-
FROM information_schema.`COLUMNS` C
|
186
|
-
WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
|
187
|
-
C.table_name = '#{connection.quote_string(table_name)}' AND
|
188
|
-
C.column_name = '#{connection.quote_string(column_name)}';
|
189
|
-
EOS
|
190
|
-
|
191
|
-
defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
|
192
|
-
defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
|
193
|
-
|
194
|
-
{
|
195
|
-
charset: defaults["character_set_name"],
|
196
|
-
collation: defaults["collation_name"]
|
197
|
-
}
|
198
108
|
else
|
199
|
-
{}
|
109
|
+
@options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@sql_type} in field #{model}##{@name}")
|
110
|
+
@options[:collation] and warne("collation may only given for :string and :text fields for SQL type #{@sql_type} in field #{model}##{@name}")
|
200
111
|
end
|
201
|
-
end
|
202
112
|
|
203
|
-
|
204
|
-
|
113
|
+
@options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
|
114
|
+
|
115
|
+
@sql_options = @options.slice(*SQL_OPTIONS)
|
205
116
|
end
|
206
117
|
|
207
|
-
|
208
|
-
|
118
|
+
# returns the attributes for schema migrations as a Hash
|
119
|
+
# omits name and position since those are meta-data above the schema
|
120
|
+
# omits keys with nil values
|
121
|
+
def schema_attributes(col_spec)
|
122
|
+
@sql_options.merge(type: @type).tap do |attrs|
|
123
|
+
attrs[:default] = Column.deserialize_default_value(col_spec, @sql_type, attrs[:default])
|
124
|
+
end.compact
|
209
125
|
end
|
210
126
|
end
|
211
127
|
end
|
@@ -5,21 +5,21 @@ module DeclareSchema
|
|
5
5
|
class ForeignKeyDefinition
|
6
6
|
include Comparable
|
7
7
|
|
8
|
-
attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
|
8
|
+
attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :options, :on_delete_cascade
|
9
9
|
|
10
10
|
def initialize(model, foreign_key, options = {})
|
11
11
|
@model = model
|
12
|
-
@foreign_key = foreign_key.presence
|
12
|
+
@foreign_key = foreign_key.to_s.presence
|
13
13
|
@options = options
|
14
14
|
|
15
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] ||
|
18
|
-
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column:
|
19
|
-
@constraint_name = options[:constraint_name] || @index_name || ''
|
20
|
-
@on_delete_cascade = options[:dependent] == :delete
|
16
|
+
@parent_table_name = options[:parent_table]&.to_s
|
17
|
+
@foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
|
18
|
+
@index_name = options[:index_name]&.to_s || model.connection.index_name(model.table_name, column: @foreign_key_name)
|
21
19
|
|
22
20
|
# Empty constraint lets mysql generate the name
|
21
|
+
@constraint_name = options[:constraint_name]&.to_s || @index_name&.to_s || ''
|
22
|
+
@on_delete_cascade = options[:dependent] == :delete
|
23
23
|
end
|
24
24
|
|
25
25
|
class << self
|
@@ -28,11 +28,12 @@ module DeclareSchema
|
|
28
28
|
constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
|
29
29
|
|
30
30
|
constraints.map do |fkc|
|
31
|
-
options = {}
|
32
31
|
name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
|
33
|
-
options
|
34
|
-
|
35
|
-
|
32
|
+
options = {
|
33
|
+
constraint_name: name,
|
34
|
+
parent_table: parent_table,
|
35
|
+
foreign_key: foreign_key
|
36
|
+
}
|
36
37
|
options[:dependent] = :delete if fkc['ON DELETE CASCADE']
|
37
38
|
|
38
39
|
new(model, foreign_key, options)
|
@@ -40,21 +41,37 @@ module DeclareSchema
|
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
44
|
+
# returns the parent class as a Class object
|
45
|
+
# or nil if no :class_name option given
|
46
|
+
def parent_class
|
47
|
+
if (class_name = options[:class_name])
|
48
|
+
if class_name.is_a?(Class)
|
49
|
+
class_name
|
50
|
+
else
|
51
|
+
class_name.to_s.constantize
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
43
56
|
def parent_table_name
|
44
57
|
@parent_table_name ||=
|
45
|
-
|
46
|
-
|
47
|
-
klass.try(:table_name)
|
48
|
-
end || foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
|
58
|
+
parent_class&.try(:table_name) ||
|
59
|
+
foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
|
49
60
|
end
|
50
61
|
|
51
|
-
attr_writer :parent_table_name
|
52
|
-
|
53
62
|
def to_add_statement
|
54
|
-
|
55
|
-
|
63
|
+
"add_foreign_key(#{@child_table.inspect}, #{parent_table_name.inspect}, " +
|
64
|
+
"column: #{@foreign_key_name.inspect}, name: #{@constraint_name.inspect})"
|
65
|
+
end
|
66
|
+
|
67
|
+
def <=>(rhs)
|
68
|
+
key <=> rhs.send(:key)
|
56
69
|
end
|
57
70
|
|
71
|
+
alias eql? ==
|
72
|
+
|
73
|
+
private
|
74
|
+
|
58
75
|
def key
|
59
76
|
@key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
|
60
77
|
end
|
@@ -62,12 +79,6 @@ module DeclareSchema
|
|
62
79
|
def hash
|
63
80
|
key.hash
|
64
81
|
end
|
65
|
-
|
66
|
-
def <=>(rhs)
|
67
|
-
key <=> rhs.key
|
68
|
-
end
|
69
|
-
|
70
|
-
alias eql? ==
|
71
82
|
end
|
72
83
|
end
|
73
84
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_record'
|
4
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
5
|
|
5
6
|
module Generators
|
6
7
|
module DeclareSchema
|
@@ -116,20 +117,6 @@ module Generators
|
|
116
117
|
ActiveRecord::Base.connection
|
117
118
|
end
|
118
119
|
|
119
|
-
def fix_native_types(types)
|
120
|
-
case connection.class.name
|
121
|
-
when /mysql/i
|
122
|
-
types[:integer][:limit] ||= 11
|
123
|
-
types[:text][:limit] ||= 0xffff
|
124
|
-
types[:binary][:limit] ||= 0xffff
|
125
|
-
end
|
126
|
-
types
|
127
|
-
end
|
128
|
-
|
129
|
-
def native_types
|
130
|
-
@native_types ||= fix_native_types(connection.native_database_types)
|
131
|
-
end
|
132
|
-
|
133
120
|
def before_generating_migration(&block)
|
134
121
|
block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
|
135
122
|
@before_generating_migration_callback = block
|
@@ -299,7 +286,7 @@ module Generators
|
|
299
286
|
"drop_table :#{t}"
|
300
287
|
end * "\n"
|
301
288
|
undo_drops = to_drop.map do |t|
|
302
|
-
|
289
|
+
add_table_back(t)
|
303
290
|
end * "\n\n"
|
304
291
|
|
305
292
|
creates = to_create.map do |t|
|
@@ -345,7 +332,7 @@ module Generators
|
|
345
332
|
disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
|
346
333
|
table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
|
347
334
|
field_definitions = [
|
348
|
-
|
335
|
+
("t.integer :id, limit: 8, auto_increment: false, primary_key: true" if disable_auto_increment),
|
349
336
|
*(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
|
350
337
|
].compact
|
351
338
|
|
@@ -386,12 +373,12 @@ module Generators
|
|
386
373
|
end
|
387
374
|
|
388
375
|
def create_constraints(model)
|
389
|
-
model.constraint_specs.map { |fk| fk.to_add_statement
|
376
|
+
model.constraint_specs.map { |fk| fk.to_add_statement }
|
390
377
|
end
|
391
378
|
|
392
379
|
def create_field(field_spec, field_name_width)
|
393
|
-
options = fk_field_options(field_spec.model, field_spec.name)
|
394
|
-
args = [field_spec.name.inspect] + format_options(options
|
380
|
+
options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
|
381
|
+
args = [field_spec.name.inspect] + format_options(options.compact)
|
395
382
|
format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
|
396
383
|
end
|
397
384
|
|
@@ -429,8 +416,8 @@ module Generators
|
|
429
416
|
adds = to_add.map do |c|
|
430
417
|
args =
|
431
418
|
if (spec = model.field_specs[c])
|
432
|
-
options = fk_field_options(model, c)
|
433
|
-
[":#{spec.sql_type}", *format_options(options
|
419
|
+
options = spec.sql_options.merge(fk_field_options(model, c))
|
420
|
+
[":#{spec.sql_type}", *format_options(options.compact)]
|
434
421
|
else
|
435
422
|
[":integer"]
|
436
423
|
end
|
@@ -444,34 +431,28 @@ module Generators
|
|
444
431
|
"remove_column :#{new_table_name}, :#{c}"
|
445
432
|
end
|
446
433
|
undo_removes = to_remove.map do |c|
|
447
|
-
|
434
|
+
add_column_back(model, current_table_name, c)
|
448
435
|
end
|
449
436
|
|
450
437
|
old_names = to_rename.invert
|
451
438
|
changes = []
|
452
439
|
undo_changes = []
|
453
|
-
to_change.each do |
|
454
|
-
|
455
|
-
|
456
|
-
spec
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
change_spec[:collation] = spec.collation unless spec.collation.nil?
|
468
|
-
|
469
|
-
changes << "change_column :#{new_table_name}, :#{c}, " +
|
470
|
-
([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
|
471
|
-
back = change_column_back(current_table_name, col_name)
|
472
|
-
undo_changes << back unless back.blank?
|
440
|
+
to_change.each do |col_name_to_change|
|
441
|
+
orig_col_name = old_names[col_name_to_change] || col_name_to_change
|
442
|
+
column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
|
443
|
+
spec = model.field_specs[col_name_to_change] or raise "failed to find field spec for #{col_name_to_change.inspect}"
|
444
|
+
spec_attrs = spec.schema_attributes(column)
|
445
|
+
column_declaration = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
446
|
+
col_attrs = column_declaration.schema_attributes
|
447
|
+
normalized_schema_attrs = spec_attrs.merge(fk_field_options(model, col_name_to_change))
|
448
|
+
|
449
|
+
if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
|
450
|
+
type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
|
451
|
+
changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
|
452
|
+
type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
|
453
|
+
undo_changes << change_column_back(model, current_table_name, orig_col_name)
|
473
454
|
end
|
474
|
-
end
|
455
|
+
end
|
475
456
|
|
476
457
|
index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
|
477
458
|
fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
@@ -485,26 +466,26 @@ module Generators
|
|
485
466
|
[[], []]
|
486
467
|
end
|
487
468
|
|
488
|
-
[(renames + adds + removes + changes)
|
469
|
+
[(renames + adds + removes + changes) * "\n",
|
489
470
|
(undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
|
490
|
-
index_changes
|
491
|
-
undo_index_changes
|
492
|
-
fk_changes
|
493
|
-
undo_fk_changes
|
494
|
-
table_options_changes
|
495
|
-
undo_table_options_changes
|
471
|
+
index_changes * "\n",
|
472
|
+
undo_index_changes * "\n",
|
473
|
+
fk_changes * "\n",
|
474
|
+
undo_fk_changes * "\n",
|
475
|
+
table_options_changes * "\n",
|
476
|
+
undo_table_options_changes * "\n"]
|
496
477
|
end
|
497
478
|
|
498
479
|
def change_indexes(model, old_table_name, to_remove)
|
499
|
-
return [[], []]
|
480
|
+
Migrator.disable_constraints and return [[], []]
|
500
481
|
|
501
482
|
new_table_name = model.table_name
|
502
483
|
existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
|
503
484
|
model_indexes_with_equivalents = model.index_definitions_with_primary_key
|
504
485
|
model_indexes = model_indexes_with_equivalents.map do |i|
|
505
486
|
if i.explicit_name.nil?
|
506
|
-
if
|
507
|
-
i.with_name(
|
487
|
+
if (existing = existing_indexes.find { |e| i != e && e.equivalent?(i) })
|
488
|
+
i.with_name(existing.name)
|
508
489
|
end
|
509
490
|
end || i
|
510
491
|
end
|
@@ -514,15 +495,13 @@ module Generators
|
|
514
495
|
end
|
515
496
|
model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
516
497
|
|
517
|
-
add_indexes_init = model_indexes - existing_indexes
|
518
|
-
drop_indexes_init = existing_indexes - model_indexes
|
519
498
|
undo_add_indexes = []
|
520
|
-
|
521
|
-
add_indexes = add_indexes_init.map do |i|
|
499
|
+
add_indexes = (model_indexes - existing_indexes).map do |i|
|
522
500
|
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
523
501
|
i.to_add_statement(new_table_name, existing_has_primary_key)
|
524
502
|
end
|
525
|
-
|
503
|
+
undo_drop_indexes = []
|
504
|
+
drop_indexes = (existing_indexes - model_indexes).map do |i|
|
526
505
|
undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
|
527
506
|
drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
528
507
|
end.compact
|
@@ -538,51 +517,41 @@ module Generators
|
|
538
517
|
end
|
539
518
|
|
540
519
|
def change_foreign_key_constraints(model, old_table_name)
|
541
|
-
ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise 'SQLite does not support foreign keys'
|
542
|
-
return [[], []]
|
520
|
+
ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
|
521
|
+
Migrator.disable_indexing and return [[], []]
|
543
522
|
|
544
523
|
new_table_name = model.table_name
|
545
524
|
existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
|
546
525
|
model_fks = model.constraint_specs
|
547
|
-
add_fks = model_fks - existing_fks
|
548
|
-
drop_fks = existing_fks - model_fks
|
549
|
-
undo_add_fks = []
|
550
|
-
undo_drop_fks = []
|
551
526
|
|
552
|
-
|
527
|
+
undo_add_fks = []
|
528
|
+
add_fks = (model_fks - existing_fks).map do |fk|
|
553
529
|
# next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
|
554
|
-
undo_add_fks << remove_foreign_key(old_table_name, fk.
|
530
|
+
undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
|
555
531
|
fk.to_add_statement
|
556
|
-
end
|
532
|
+
end
|
557
533
|
|
558
|
-
|
534
|
+
undo_drop_fks = []
|
535
|
+
drop_fks = (existing_fks - model_fks).map do |fk|
|
559
536
|
undo_drop_fks << fk.to_add_statement
|
560
|
-
remove_foreign_key(new_table_name, fk.
|
537
|
+
remove_foreign_key(new_table_name, fk.constraint_name)
|
561
538
|
end
|
562
539
|
|
563
540
|
[drop_fks + add_fks, undo_add_fks + undo_drop_fks]
|
564
541
|
end
|
565
542
|
|
566
543
|
def remove_foreign_key(old_table_name, fk_name)
|
567
|
-
"remove_foreign_key(
|
544
|
+
"remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
|
568
545
|
end
|
569
546
|
|
570
|
-
def format_options(options
|
547
|
+
def format_options(options)
|
571
548
|
options.map do |k, v|
|
572
|
-
if !changing && ((k == :limit && type == :decimal) || (k == :null && v == true))
|
573
|
-
next
|
574
|
-
end
|
575
|
-
|
576
|
-
if !::DeclareSchema::Model::FieldSpec.mysql_text_limits? && k == :limit && type == :text
|
577
|
-
next
|
578
|
-
end
|
579
|
-
|
580
549
|
if k.is_a?(Symbol)
|
581
550
|
"#{k}: #{v.inspect}"
|
582
551
|
else
|
583
552
|
"#{k.inspect} => #{v.inspect}"
|
584
553
|
end
|
585
|
-
end
|
554
|
+
end
|
586
555
|
end
|
587
556
|
|
588
557
|
def fk_field_options(model, field_name)
|
@@ -620,7 +589,33 @@ module Generators
|
|
620
589
|
end
|
621
590
|
end
|
622
591
|
|
623
|
-
|
592
|
+
def with_previous_model_table_name(model, table_name)
|
593
|
+
model_table_name, model.table_name = model.table_name, table_name
|
594
|
+
yield
|
595
|
+
ensure
|
596
|
+
model.table_name = model_table_name
|
597
|
+
end
|
598
|
+
|
599
|
+
def add_column_back(model, current_table_name, col_name)
|
600
|
+
with_previous_model_table_name(model, current_table_name) do
|
601
|
+
column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
|
602
|
+
col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
603
|
+
schema_attributes = col_spec.schema_attributes
|
604
|
+
type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
|
605
|
+
["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
def change_column_back(model, current_table_name, col_name)
|
610
|
+
with_previous_model_table_name(model, current_table_name) do
|
611
|
+
column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
|
612
|
+
col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
613
|
+
schema_attributes = col_spec.schema_attributes
|
614
|
+
type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
|
615
|
+
["change_column #{current_table_name.to_sym.inspect}", col_name.to_sym.inspect, type.to_sym.inspect, *format_options(schema_attributes)].join(', ')
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
624
619
|
def default_collation_from_charset(charset)
|
625
620
|
case charset
|
626
621
|
when "utf8"
|
@@ -630,64 +625,47 @@ module Generators
|
|
630
625
|
end
|
631
626
|
end
|
632
627
|
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
628
|
+
SchemaDumper = case Rails::VERSION::MAJOR
|
629
|
+
when 4
|
630
|
+
ActiveRecord::SchemaDumper
|
631
|
+
else
|
632
|
+
ActiveRecord::ConnectionAdapters::SchemaDumper
|
633
|
+
end
|
634
|
+
|
635
|
+
def add_table_back(table)
|
636
|
+
dumped_schema_stream = StringIO.new
|
637
|
+
SchemaDumper.send(:new, ActiveRecord::Base.connection).send(:table, table, dumped_schema_stream)
|
642
638
|
|
643
|
-
|
639
|
+
dumped_schema = dumped_schema_stream.string.strip.gsub!("\n ", "\n")
|
644
640
|
if connection.class.name.match?(/mysql/i)
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
end
|
649
|
-
default_charset = result[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{result.inspect}"
|
650
|
-
default_collation = result[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
|
651
|
-
raise "unable to find collation in #{result.inspect} or charset #{default_charset.inspect}"
|
652
|
-
result = result.split("\n").map do |line|
|
653
|
-
if line['t.text'] || line['t.string']
|
654
|
-
if !line['charset: ']
|
655
|
-
if line['collation: ']
|
656
|
-
line = line.sub('collation: ', "charset: #{default_charset.inspect}, collation: ")
|
657
|
-
else
|
658
|
-
line += ", charset: #{default_charset.inspect}"
|
659
|
-
end
|
660
|
-
end
|
661
|
-
line['collation: '] or line += ", collation: #{default_collation.inspect}"
|
662
|
-
end
|
663
|
-
line
|
664
|
-
end.join("\n")
|
641
|
+
fix_mysql_charset_and_collation(dumped_schema)
|
642
|
+
else
|
643
|
+
dumped_schema
|
665
644
|
end
|
666
|
-
result
|
667
645
|
end
|
668
646
|
|
669
|
-
|
670
|
-
|
671
|
-
if
|
672
|
-
|
673
|
-
|
674
|
-
elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{column}"(?:,\s+(.*?)$)?/m))
|
675
|
-
# Sexy migration
|
676
|
-
_, string_type, options = *md
|
677
|
-
type = ":#{string_type}"
|
647
|
+
# TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
|
648
|
+
def fix_mysql_charset_and_collation(dumped_schema)
|
649
|
+
if !dumped_schema['options: ']
|
650
|
+
dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
|
651
|
+
"COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
|
678
652
|
end
|
679
|
-
|
680
|
-
[
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
653
|
+
default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
|
654
|
+
default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
|
655
|
+
raise "unable to find collation in #{dumped_schema.inspect} or charset #{default_charset.inspect}"
|
656
|
+
dumped_schema.split("\n").map do |line|
|
657
|
+
if line['t.text'] || line['t.string']
|
658
|
+
if !line['charset: ']
|
659
|
+
if line['collation: ']
|
660
|
+
line.sub!('collation: ', "charset: #{default_charset.inspect}, collation: ")
|
661
|
+
else
|
662
|
+
line << ", charset: #{default_charset.inspect}"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
line['collation: '] or line << ", collation: #{default_collation.inspect}"
|
666
|
+
end
|
667
|
+
line
|
668
|
+
end.join("\n")
|
691
669
|
end
|
692
670
|
end
|
693
671
|
end
|