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.
@@ -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