declare_schema 0.5.0 → 0.6.4

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +60 -0
  3. data/.gitignore +1 -0
  4. data/Appraisals +21 -4
  5. data/CHANGELOG.md +46 -0
  6. data/Gemfile +1 -2
  7. data/Gemfile.lock +4 -6
  8. data/README.md +3 -3
  9. data/Rakefile +17 -4
  10. data/bin/declare_schema +1 -1
  11. data/declare_schema.gemspec +1 -1
  12. data/gemfiles/rails_4_mysql.gemfile +22 -0
  13. data/gemfiles/{rails_4.gemfile → rails_4_sqlite.gemfile} +1 -2
  14. data/gemfiles/rails_5_mysql.gemfile +22 -0
  15. data/gemfiles/{rails_5.gemfile → rails_5_sqlite.gemfile} +1 -2
  16. data/gemfiles/rails_6_mysql.gemfile +22 -0
  17. data/gemfiles/{rails_6.gemfile → rails_6_sqlite.gemfile} +2 -3
  18. data/lib/declare_schema/command.rb +10 -3
  19. data/lib/declare_schema/model.rb +1 -1
  20. data/lib/declare_schema/model/field_spec.rb +18 -14
  21. data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
  22. data/lib/declare_schema/model/table_options_definition.rb +8 -6
  23. data/lib/declare_schema/version.rb +1 -1
  24. data/lib/generators/declare_schema/migration/migrator.rb +80 -34
  25. data/spec/lib/declare_schema/field_spec_spec.rb +69 -0
  26. data/spec/lib/declare_schema/generator_spec.rb +4 -2
  27. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +8 -2
  28. data/spec/lib/declare_schema/migration_generator_spec.rb +286 -158
  29. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
  30. data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -5
  31. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +19 -29
  32. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +17 -22
  33. data/spec/support/acceptance_spec_helpers.rb +3 -3
  34. metadata +15 -10
  35. data/.travis.yml +0 -37
@@ -88,7 +88,7 @@ module DeclareSchema
88
88
  add_formatting_for_field(name, type)
89
89
  add_validations_for_field(name, type, args, options)
90
90
  add_index_for_field(name, args, options)
91
- field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, options)
91
+ field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, position: field_specs.size, **options)
92
92
  attr_order << name unless attr_order.include?(name)
93
93
  end
94
94
 
@@ -13,7 +13,6 @@ module DeclareSchema
13
13
  MYSQL_TEXT_LIMITS_ASCENDING = [MYSQL_TINYTEXT_LIMIT, MYSQL_TEXT_LIMIT, MYSQL_MEDIUMTEXT_LIMIT, MYSQL_LONGTEXT_LIMIT].freeze
14
14
 
15
15
  class << self
16
- # method for easy stubbing in tests
17
16
  def mysql_text_limits?
18
17
  if defined?(@mysql_text_limits)
19
18
  @mysql_text_limits
@@ -33,7 +32,8 @@ module DeclareSchema
33
32
 
34
33
  attr_reader :model, :name, :type, :position, :options
35
34
 
36
- def initialize(model, name, type, options = {})
35
+ def initialize(model, name, type, position: 0, **options)
36
+ # TODO: TECH-5116
37
37
  # Invoca change - searching for the primary key was causing an additional database read on every model load. Assume
38
38
  # "id" which works for invoca.
39
39
  # raise ArgumentError, "you cannot provide a field spec for the primary key" if name == model.primary_key
@@ -43,9 +43,8 @@ module DeclareSchema
43
43
  @name = name.to_sym
44
44
  type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
45
45
  @type = type
46
- position_option = options.delete(:position)
46
+ @position = position
47
47
  @options = options
48
-
49
48
  case type
50
49
  when :text
51
50
  @options[:default] and raise "default may not be given for :text field #{model}##{@name}"
@@ -54,11 +53,20 @@ module DeclareSchema
54
53
  end
55
54
  when :string
56
55
  @options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
56
+ when :bigint
57
+ @type = :integer
58
+ @options = options.merge(limit: 8)
59
+ end
60
+
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
57
66
  else
58
- @options[:collation] and raise "collation may only given for :string and :text fields"
59
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"
60
69
  end
61
- @position = position_option || model.field_specs.length
62
70
  end
63
71
 
64
72
  TYPE_SYNONYMS = { timestamp: :datetime }.freeze
@@ -105,16 +113,12 @@ module DeclareSchema
105
113
  @options[:default]
106
114
  end
107
115
 
108
- def collation
109
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
110
- (@options[:collation] || model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation).to_s
111
- end
116
+ def charset
117
+ @options[:charset]
112
118
  end
113
119
 
114
- def charset
115
- if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
116
- (@options[:charset] || model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset).to_s
117
- end
120
+ def collation
121
+ @options[:collation]
118
122
  end
119
123
 
120
124
  def same_type?(col_spec)
@@ -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
@@ -26,12 +26,14 @@ module DeclareSchema
26
26
 
27
27
  def mysql_table_options(connection, table_name)
28
28
  database = connection.current_database
29
- defaults = connection.select_one(<<~EOS)
29
+ defaults = connection.select_one(<<~EOS) or raise "no defaults found for table #{table_name}"
30
30
  SELECT CCSA.character_set_name, CCSA.collation_name
31
- FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
32
- WHERE CCSA.collation_name = T.table_collation AND
33
- T.table_schema = '#{connection.quote_string(database)}' AND
34
- T.table_name = '#{connection.quote_string(table_name)}';
31
+ FROM information_schema.TABLES as T
32
+ JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY as CCSA
33
+ ON CCSA.collation_name = T.table_collation
34
+ WHERE
35
+ T.table_schema = '#{connection.quote_string(database)}' AND
36
+ T.table_name = '#{connection.quote_string(table_name)}'
35
37
  EOS
36
38
 
37
39
  defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
@@ -75,7 +77,7 @@ module DeclareSchema
75
77
  alias to_s settings
76
78
 
77
79
  def alter_table_statement
78
- statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{to_s};"
80
+ statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{to_s}"
79
81
  "execute #{statement.inspect}"
80
82
  end
81
83
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.4"
5
5
  end
@@ -37,11 +37,9 @@ module Generators
37
37
 
38
38
  def field_specs
39
39
  i = 0
40
- foreign_keys.reduce({}) do |h, v|
41
- # some trickery to avoid an infinite loop when FieldSpec#initialize tries to call model.field_specs
42
- h[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :integer, position: i, null: false)
40
+ foreign_keys.each_with_object({}) do |v, result|
41
+ result[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :integer, position: i, null: false)
43
42
  i += 1
44
- h
45
43
  end
46
44
  end
47
45
 
@@ -73,8 +71,8 @@ module Generators
73
71
  class Migrator
74
72
  class Error < RuntimeError; end
75
73
 
76
- DEFAULT_CHARSET = :utf8mb4
77
- DEFAULT_COLLATION = :utf8mb4_general
74
+ DEFAULT_CHARSET = "utf8mb4"
75
+ DEFAULT_COLLATION = "utf8mb4_bin"
78
76
 
79
77
  @ignore_models = []
80
78
  @ignore_tables = []
@@ -84,9 +82,18 @@ module Generators
84
82
  @default_collation = DEFAULT_COLLATION
85
83
 
86
84
  class << self
87
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
88
- :active_record_class, :default_charset, :default_collation
89
- attr_reader :before_generating_migration_callback
85
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints
86
+ attr_reader :active_record_class, :default_charset, :default_collation, :before_generating_migration_callback
87
+
88
+ def default_charset=(charset)
89
+ charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
90
+ @default_charset = charset
91
+ end
92
+
93
+ def default_collation=(collation)
94
+ collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
95
+ @default_collation = collation
96
+ end
90
97
 
91
98
  def active_record_class
92
99
  @active_record_class.is_a?(Class) or @active_record_class = @active_record_class.to_s.constantize
@@ -379,7 +386,7 @@ module Generators
379
386
  end
380
387
 
381
388
  def create_constraints(model)
382
- model.constraint_specs.map { |fk| fk.to_add_statement(model.table_name) }
389
+ model.constraint_specs.map { |fk| fk.to_add_statement }
383
390
  end
384
391
 
385
392
  def create_field(field_spec, field_name_width)
@@ -427,7 +434,7 @@ module Generators
427
434
  else
428
435
  [":integer"]
429
436
  end
430
- "add_column :#{new_table_name}, :#{c}, #{args.join(', ')}"
437
+ ["add_column :#{new_table_name}, :#{c}", *args].join(', ')
431
438
  end
432
439
  undo_adds = to_add.map do |c|
433
440
  "remove_column :#{new_table_name}, :#{c}"
@@ -456,8 +463,8 @@ module Generators
456
463
  change_spec[:scale] = spec.scale unless spec.scale.nil?
457
464
  change_spec[:null] = spec.null unless spec.null && col.null
458
465
  change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
459
- change_spec[:collation] = spec.collation unless spec.collation.nil?
460
466
  change_spec[:charset] = spec.charset unless spec.charset.nil?
467
+ change_spec[:collation] = spec.collation unless spec.collation.nil?
461
468
 
462
469
  changes << "change_column :#{new_table_name}, :#{c}, " +
463
470
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -466,7 +473,7 @@ module Generators
466
473
  end
467
474
  end.compact
468
475
 
469
- index_changes, undo_index_changes = change_indexes(model, current_table_name)
476
+ index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
470
477
  fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
471
478
  [[], []]
472
479
  else
@@ -488,7 +495,7 @@ module Generators
488
495
  undo_table_options_changes * "\n"]
489
496
  end
490
497
 
491
- def change_indexes(model, old_table_name)
498
+ def change_indexes(model, old_table_name, to_remove)
492
499
  return [[], []] if Migrator.disable_constraints
493
500
 
494
501
  new_table_name = model.table_name
@@ -501,7 +508,10 @@ module Generators
501
508
  end
502
509
  end || i
503
510
  end
504
- existing_has_primary_key = existing_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
511
+ existing_has_primary_key = existing_indexes.any? do |i|
512
+ i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME &&
513
+ !i.fields.all? { |f| to_remove.include?(f) } # if we're removing the primary key column(s), the primary key index will be removed too
514
+ end
505
515
  model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
506
516
 
507
517
  add_indexes_init = model_indexes - existing_indexes
@@ -541,30 +551,31 @@ module Generators
541
551
 
542
552
  add_fks.map! do |fk|
543
553
  # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
544
- undo_add_fks << remove_foreign_key(old_table_name, fk.options[:constraint_name])
554
+ undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
545
555
  fk.to_add_statement
546
556
  end.compact
547
557
 
548
558
  drop_fks.map! do |fk|
549
559
  undo_drop_fks << fk.to_add_statement
550
- remove_foreign_key(new_table_name, fk.options[:constraint_name])
560
+ remove_foreign_key(new_table_name, fk.constraint_name)
551
561
  end
552
562
 
553
563
  [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
554
564
  end
555
565
 
556
566
  def remove_foreign_key(old_table_name, fk_name)
557
- "remove_foreign_key('#{old_table_name}', name: '#{fk_name}')"
567
+ "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
558
568
  end
559
569
 
560
570
  def format_options(options, type, changing: false)
561
571
  options.map do |k, v|
562
- unless changing
563
- next if k == :limit && (type == :decimal || v == native_types[type][:limit])
564
- next if k == :null && v == true
572
+ if !changing && ((k == :limit && type == :decimal) || (k == :null && v == true))
573
+ next
565
574
  end
566
575
 
567
- next if k == :limit && type == :text && !::DeclareSchema::Model::FieldSpec.mysql_text_limits?
576
+ if !::DeclareSchema::Model::FieldSpec.mysql_text_limits? && k == :limit && type == :text
577
+ next
578
+ end
568
579
 
569
580
  if k.is_a?(Symbol)
570
581
  "#{k}: #{v.inspect}"
@@ -578,9 +589,9 @@ module Generators
578
589
  foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }
579
590
  if foreign_key && (parent_table = foreign_key.parent_table_name)
580
591
  parent_columns = connection.columns(parent_table) rescue []
581
- pk_limit =
592
+ pk_limit =
582
593
  if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
583
- if Rails::VERSION::MAJOR <= 4
594
+ if Rails::VERSION::MAJOR < 5
584
595
  pk_column.cast_type.limit
585
596
  else
586
597
  pk_column.limit
@@ -609,6 +620,16 @@ module Generators
609
620
  end
610
621
  end
611
622
 
623
+ # TODO: TECH-4814 remove all methods from here through end of file
624
+ def default_collation_from_charset(charset)
625
+ case charset
626
+ when "utf8"
627
+ "utf8_general_ci"
628
+ when "utf8mb4"
629
+ "utf8mb4_general_ci"
630
+ end
631
+ end
632
+
612
633
  def revert_table(table)
613
634
  res = StringIO.new
614
635
  schema_dumper_klass = case Rails::VERSION::MAJOR
@@ -618,30 +639,55 @@ module Generators
618
639
  ActiveRecord::ConnectionAdapters::SchemaDumper
619
640
  end
620
641
  schema_dumper_klass.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
621
- res.string.strip.gsub("\n ", "\n")
642
+
643
+ result = res.string.strip.gsub("\n ", "\n")
644
+ 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")
665
+ end
666
+ result
622
667
  end
623
668
 
624
- def column_options_from_reverted_table(table, col_name)
669
+ def column_options_from_reverted_table(table, column)
625
670
  revert = revert_table(table)
626
- if (md = revert.match(/\s*t\.column\s+"#{col_name}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
671
+ if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
627
672
  # Ugly migration
628
673
  _, type, options = *md
629
- elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{col_name}"(?:,\s+(.*?)$)?/m))
674
+ elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{column}"(?:,\s+(.*?)$)?/m))
630
675
  # Sexy migration
631
- _, type, options = *md
632
- type = ":#{type}"
676
+ _, string_type, options = *md
677
+ type = ":#{string_type}"
633
678
  end
679
+ type or raise "unable to find column options for #{table}.#{column} in #{revert.inspect}"
634
680
  [type, options]
635
681
  end
636
682
 
637
- def change_column_back(table, col_name)
638
- type, options = column_options_from_reverted_table(table, col_name)
639
- "change_column :#{table}, :#{col_name}, #{type}#{', ' + options.strip if options}"
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(', ')
640
686
  end
641
687
 
642
688
  def revert_column(table, column)
643
689
  type, options = column_options_from_reverted_table(table, column)
644
- "add_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
690
+ ["add_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
645
691
  end
646
692
  end
647
693
  end