declare_schema 0.6.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = options.merge(limit: 8)
77
+ @options[:limit] = 8
59
78
  end
60
79
 
61
- if type.in?([:text, :string])
62
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
63
- @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
64
- @options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
65
- end
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[:charset] and raise "charset may only given for :string and :text fields"
68
- @options[:collation] and raise "collation may only given for :string and :text fields"
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
- TYPE_SYNONYMS = { timestamp: :datetime }.freeze
73
-
74
- SQLITE_COLUMN_CLASS =
75
- begin
76
- ActiveRecord::ConnectionAdapters::SQLiteColumn
77
- rescue NameError
78
- NilClass
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
- def sql_type
82
- @options[:sql_type] || begin
83
- if native_type?(type)
84
- type
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
- col_value = col_spec.send(k)
161
- if col_value.nil? && native_type
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
- def native_type?(type)
204
- type.to_sym != :primary_key && native_types.has_key?(type)
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
- def native_types
208
- Generators::DeclareSchema::Migration::Migrator.native_types
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] || 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
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[:constraint_name] = name
34
- options[:parent_table] = parent_table
35
- options[:foreign_key] = foreign_key
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
- 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
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
- 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}"
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.1"
5
5
  end
@@ -96,7 +96,7 @@ module DeclareSchema
96
96
  end
97
97
  end
98
98
  end
99
- rescue ::DeclareSchema::Model::FieldSpec::UnknownSqlTypeError => ex
99
+ rescue ::DeclareSchema::UnknownSqlTypeError => ex
100
100
  say "Invalid field type: #{ex}"
101
101
  end
102
102
 
@@ -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
- revert_table(t)
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
- disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
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(model.table_name) }
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).merge(field_spec.sql_options)
394
- args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
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).merge(spec.sql_options)
433
- [":#{spec.sql_type}", *format_options(options, spec.sql_type)]
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
- revert_column(current_table_name, c)
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 |c|
454
- col_name = old_names[c] || c
455
- col = db_columns[col_name]
456
- spec = model.field_specs[c]
457
- 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
458
- change_spec = fk_field_options(model, c)
459
- change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
460
- ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
461
- (spec.limit || col.limit)
462
- change_spec[:precision] = spec.precision unless spec.precision.nil?
463
- change_spec[:scale] = spec.scale unless spec.scale.nil?
464
- change_spec[:null] = spec.null unless spec.null && col.null
465
- change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
466
- change_spec[:charset] = spec.charset unless spec.charset.nil?
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.compact
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) * "\n",
469
+ [(renames + adds + removes + changes) * "\n",
489
470
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
490
- index_changes * "\n",
491
- undo_index_changes * "\n",
492
- fk_changes * "\n",
493
- undo_fk_changes * "\n",
494
- table_options_changes * "\n",
495
- undo_table_options_changes * "\n"]
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 [[], []] if Migrator.disable_constraints
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 ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
507
- i.with_name(ex.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
- undo_drop_indexes = []
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
- drop_indexes = drop_indexes_init.map do |i|
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 [[], []] if Migrator.disable_indexing
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
- add_fks.map! do |fk|
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.options[:constraint_name])
530
+ undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
555
531
  fk.to_add_statement
556
- end.compact
532
+ end
557
533
 
558
- drop_fks.map! do |fk|
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.options[:constraint_name])
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('#{old_table_name}', name: '#{fk_name}')"
544
+ "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
568
545
  end
569
546
 
570
- def format_options(options, type, changing: false)
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.compact
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
- # TODO: TECH-4814 remove all methods from here through end of file
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
- def revert_table(table)
634
- res = StringIO.new
635
- schema_dumper_klass = case Rails::VERSION::MAJOR
636
- when 4
637
- ActiveRecord::SchemaDumper
638
- else
639
- ActiveRecord::ConnectionAdapters::SchemaDumper
640
- end
641
- schema_dumper_klass.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
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
- result = res.string.strip.gsub("\n ", "\n")
639
+ dumped_schema = dumped_schema_stream.string.strip.gsub!("\n ", "\n")
644
640
  if connection.class.name.match?(/mysql/i)
645
- if !result['options: ']
646
- result = result.sub('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
647
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
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
- def column_options_from_reverted_table(table, column)
670
- revert = revert_table(table)
671
- if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
672
- # Ugly migration
673
- _, type, options = *md
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
- type or raise "unable to find column options for #{table}.#{column} in #{revert.inspect}"
680
- [type, options]
681
- end
682
-
683
- def change_column_back(table, column)
684
- type, options = column_options_from_reverted_table(table, column)
685
- ["change_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
686
- end
687
-
688
- def revert_column(table, column)
689
- type, options = column_options_from_reverted_table(table, column)
690
- ["add_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
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