declare_schema 0.5.0 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
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