declare_schema 0.3.0 → 0.5.0.pre.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.
@@ -54,6 +54,9 @@ module DeclareSchema
54
54
  end
55
55
  when :string
56
56
  @options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
57
+ else
58
+ @options[:collation] and raise "collation may only given for :string and :text fields"
59
+ @options[:charset] and raise "charset may only given for :string and :text fields"
57
60
  end
58
61
  @position = position_option || model.field_specs.length
59
62
  end
@@ -102,6 +105,18 @@ module DeclareSchema
102
105
  @options[:default]
103
106
  end
104
107
 
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
112
+ end
113
+
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
118
+ end
119
+
105
120
  def same_type?(col_spec)
106
121
  type = sql_type
107
122
  normalized_type = TYPE_SYNONYMS[type] || type
@@ -109,36 +124,77 @@ module DeclareSchema
109
124
  normalized_type == normalized_col_spec_type
110
125
  end
111
126
 
112
- def different_to?(col_spec)
113
- !same_type?(col_spec) ||
114
- begin
115
- native_type = native_types[type]
116
- check_attributes = [:null, :default]
117
- 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
118
- check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
119
- check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
120
- (sql_type == :text && self.class.mysql_text_limits?)
121
- check_attributes.any? do |k|
122
- if k == :default
123
- case Rails::VERSION::MAJOR
124
- when 4
125
- col_spec.type_cast_from_database(col_spec.default) != col_spec.type_cast_from_database(default)
126
- else
127
- cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
128
- cast_type.deserialize(col_spec.default) != cast_type.deserialize(default)
129
- end
130
- else
131
- col_value = col_spec.send(k)
132
- if col_value.nil? && native_type
133
- col_value = native_type[k]
134
- end
135
- col_value != send(k)
136
- end
127
+ def different_to?(table_name, col_spec)
128
+ !same_as(table_name, col_spec)
129
+ end
130
+
131
+ def same_as(table_name, col_spec)
132
+ same_type?(col_spec) &&
133
+ same_attributes?(col_spec) &&
134
+ (!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
135
+ end
136
+
137
+ private
138
+
139
+ def same_attributes?(col_spec)
140
+ native_type = native_types[type]
141
+ check_attributes = [:null, :default]
142
+ 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
143
+ check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
144
+ check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
145
+ (sql_type == :text && self.class.mysql_text_limits?)
146
+ check_attributes.all? do |k|
147
+ if k == :default
148
+ case Rails::VERSION::MAJOR
149
+ when 4
150
+ col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
151
+ else
152
+ cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
153
+ cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
154
+ end
155
+ else
156
+ col_value = col_spec.send(k)
157
+ if col_value.nil? && native_type
158
+ col_value = native_type[k]
137
159
  end
160
+ col_value == send(k)
138
161
  end
162
+ end
139
163
  end
140
164
 
141
- private
165
+ def same_charset_and_collation?(table_name, col_spec)
166
+ current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
167
+
168
+ collation == current_collation_and_charset[:collation] &&
169
+ charset == current_collation_and_charset[:charset]
170
+ end
171
+
172
+ def collation_and_charset_for_column(table_name, col_spec)
173
+ column_name = col_spec.name
174
+ connection = ActiveRecord::Base.connection
175
+
176
+ if connection.class.name.match?(/mysql/i)
177
+ database_name = connection.current_database
178
+
179
+ defaults = connection.select_one(<<~EOS)
180
+ SELECT C.character_set_name, C.collation_name
181
+ FROM information_schema.`COLUMNS` C
182
+ WHERE C.table_schema = #{connection.quote_string(database_name)} AND
183
+ C.table_name = #{connection.quote_string(table_name)} AND
184
+ C.column_name = #{connection.quote_string(column_name)};
185
+ EOS
186
+
187
+ defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
188
+ defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
189
+
190
+ {
191
+ charset: defaults["character_set_name"],
192
+ collation: defaults["collation_name"]
193
+ }
194
+ else
195
+ {}
196
+ end
197
+ end
142
198
 
143
199
  def native_type?(type)
144
200
  type.to_sym != :primary_key && native_types.has_key?(type)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class ForeignKeyDefinition
6
+ include Comparable
7
+
8
+ attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
9
+
10
+ def initialize(model, foreign_key, options = {})
11
+ @model = model
12
+ @foreign_key = foreign_key.presence
13
+ @options = options
14
+
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
21
+
22
+ # Empty constraint lets mysql generate the name
23
+ end
24
+
25
+ class << self
26
+ def for_model(model, old_table_name)
27
+ show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
28
+ constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
29
+
30
+ constraints.map do |fkc|
31
+ options = {}
32
+ 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
36
+ options[:dependent] = :delete if fkc['ON DELETE CASCADE']
37
+
38
+ new(model, foreign_key, options)
39
+ end
40
+ end
41
+ end
42
+
43
+ def parent_table_name
44
+ @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
49
+ end
50
+
51
+ attr_writer :parent_table_name
52
+
53
+ 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}"
56
+ end
57
+
58
+ def key
59
+ @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
60
+ end
61
+
62
+ def hash
63
+ key.hash
64
+ end
65
+
66
+ def <=>(rhs)
67
+ key <=> rhs.key
68
+ end
69
+
70
+ alias eql? ==
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class IndexDefinition
6
+ include Comparable
7
+
8
+ # TODO: replace `fields` with `columns` and remove alias. -Colin
9
+ attr_reader :table, :fields, :explicit_name, :name, :unique, :where
10
+ alias columns fields
11
+
12
+ class IndexNameTooLongError < RuntimeError; end
13
+
14
+ PRIMARY_KEY_NAME = "PRIMARY"
15
+ MYSQL_INDEX_NAME_MAX_LENGTH = 64
16
+
17
+ def initialize(model, fields, options = {})
18
+ @model = model
19
+ @table = options.delete(:table_name) || model.table_name
20
+ @fields = Array.wrap(fields).map(&:to_s)
21
+ @explicit_name = options[:name] unless options.delete(:allow_equivalent)
22
+ @name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
23
+ @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
24
+
25
+ if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
26
+ raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
27
+ end
28
+
29
+ if (where = options[:where])
30
+ @where = where.start_with?('(') ? where : "(#{where})"
31
+ end
32
+ end
33
+
34
+ class << self
35
+ # extract IndexSpecs from an existing table
36
+ # always includes the PRIMARY KEY index
37
+ def for_model(model, old_table_name = nil)
38
+ t = old_table_name || model.table_name
39
+
40
+ primary_key_columns = Array(model.connection.primary_key(t)).presence || sqlite_compound_primary_key(model, t) or
41
+ raise "could not find primary key for table #{t} in #{model.connection.columns(t).inspect}"
42
+
43
+ primary_key_found = false
44
+ index_definitions = model.connection.indexes(t).map do |i|
45
+ model.ignore_indexes.include?(i.name) and next
46
+ if i.name == PRIMARY_KEY_NAME
47
+ i.columns == primary_key_columns && i.unique or
48
+ raise "primary key on #{t} was not unique on #{primary_key_columns} (was unique=#{i.unique} on #{i.columns})"
49
+ primary_key_found = true
50
+ elsif i.columns == primary_key_columns && i.unique
51
+ # skip this primary key index since we'll create it below, with PRIMARY_KEY_NAME
52
+ next
53
+ end
54
+ new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name)
55
+ end.compact
56
+
57
+ if !primary_key_found
58
+ index_definitions << new(model, primary_key_columns, name: PRIMARY_KEY_NAME, unique: true, where: nil, table_name: old_table_name)
59
+ end
60
+ index_definitions
61
+ end
62
+
63
+ private
64
+
65
+ # This is the old approach which is still needed for MySQL in Rails 4 and SQLite
66
+ def sqlite_compound_primary_key(model, table)
67
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) || Rails::VERSION::MAJOR < 5 or return nil
68
+
69
+ connection = model.connection.dup
70
+
71
+ class << connection # defeat Rails MySQL driver code that skips the primary key by changing its name to a symbol
72
+ def each_hash(result)
73
+ super do |hash|
74
+ if hash[:Key_name] == PRIMARY_KEY_NAME
75
+ hash[:Key_name] = PRIMARY_KEY_NAME.to_sym
76
+ end
77
+ yield hash
78
+ end
79
+ end
80
+ end
81
+
82
+ pk_index = connection.indexes(table).find { |index| index.name.to_s == PRIMARY_KEY_NAME } or return nil
83
+
84
+ Array(pk_index.columns)
85
+ end
86
+ end
87
+
88
+ def primary_key?
89
+ name == PRIMARY_KEY_NAME
90
+ end
91
+
92
+ def to_add_statement(new_table_name, existing_primary_key = nil)
93
+ if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
94
+ to_add_primary_key_statement(new_table_name, existing_primary_key)
95
+ else
96
+ # Note: + below keeps that interpolated string from being frozen, so we can << into it.
97
+ r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
98
+ r << ", unique: true" if unique
99
+ r << ", where: '#{where}'" if where.present?
100
+ r << ", name: '#{name}'"
101
+ r
102
+ end
103
+ end
104
+
105
+ def to_add_primary_key_statement(new_table_name, existing_primary_key)
106
+ drop = "DROP PRIMARY KEY, " if existing_primary_key
107
+ statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
108
+ "execute #{statement.inspect}"
109
+ end
110
+
111
+ def to_key
112
+ @key ||= [table, fields, name, unique, where].map(&:to_s)
113
+ end
114
+
115
+ def settings
116
+ @settings ||= [table, fields, unique].map(&:to_s)
117
+ end
118
+
119
+ def hash
120
+ to_key.hash
121
+ end
122
+
123
+ def <=>(rhs)
124
+ to_key <=> rhs.to_key
125
+ end
126
+
127
+ def equivalent?(rhs)
128
+ settings == rhs.settings
129
+ end
130
+
131
+ def with_name(new_name)
132
+ self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
133
+ end
134
+
135
+ alias eql? ==
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class TableOptionsDefinition
6
+ include Comparable
7
+
8
+ TABLE_OPTIONS_TO_SQL_MAPPINGS = {
9
+ charset: 'CHARACTER SET',
10
+ collation: 'COLLATE'
11
+ }.freeze
12
+
13
+ class << self
14
+ def for_model(model, old_table_name = nil)
15
+ table_name = old_table_name || model.table_name
16
+ table_options = if model.connection.class.name.match?(/mysql/i)
17
+ mysql_table_options(model.connection, table_name)
18
+ else
19
+ {}
20
+ end
21
+
22
+ new(table_name, table_options)
23
+ end
24
+
25
+ private
26
+
27
+ def mysql_table_options(connection, table_name)
28
+ database = connection.current_database
29
+ defaults = connection.select_one(<<~EOS)
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)};
35
+ EOS
36
+
37
+ defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
38
+ defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
39
+
40
+ {
41
+ charset: defaults["character_set_name"],
42
+ collation: defaults["collation_name"]
43
+ }
44
+ end
45
+ end
46
+
47
+ attr_reader :table_name, :table_options
48
+
49
+ def initialize(table_name, table_options = {})
50
+ @table_name = table_name
51
+ @table_options = table_options
52
+ end
53
+
54
+ def to_key
55
+ @key ||= [table_name, table_options].map(&:to_s)
56
+ end
57
+
58
+ def settings
59
+ @settings ||= table_options.map { |name, value| "#{TABLE_OPTIONS_TO_SQL_MAPPINGS[name]} #{value}" if value }.compact.join(" ")
60
+ end
61
+
62
+ def hash
63
+ to_key.hash
64
+ end
65
+
66
+ def <=>(rhs)
67
+ to_key <=> rhs.to_key
68
+ end
69
+
70
+ def equivalent?(rhs)
71
+ settings == rhs.settings
72
+ end
73
+
74
+ alias eql? ==
75
+ alias to_s settings
76
+
77
+ def alter_table_statement
78
+ statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{to_s};"
79
+ "execute #{statement.inspect}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0.pre.1"
5
5
  end
@@ -45,14 +45,14 @@ module Generators
45
45
  false # no single-column primary key
46
46
  end
47
47
 
48
- def index_specs_with_primary_key
48
+ def index_definitions_with_primary_key
49
49
  [
50
- ::DeclareSchema::Model::IndexSpec.new(self, foreign_keys, unique: true, name: "PRIMARY_KEY"),
51
- ::DeclareSchema::Model::IndexSpec.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
50
+ ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME),
51
+ ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
52
52
  ]
53
53
  end
54
54
 
55
- alias_method :index_specs, :index_specs_with_primary_key
55
+ alias_method :index_definitions, :index_definitions_with_primary_key
56
56
 
57
57
  def ignore_indexes
58
58
  []
@@ -60,8 +60,8 @@ module Generators
60
60
 
61
61
  def constraint_specs
62
62
  [
63
- ::DeclareSchema::Model::ForeignKeySpec.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
64
- ::DeclareSchema::Model::ForeignKeySpec.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
63
+ ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
64
+ ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
65
65
  ]
66
66
  end
67
67
  end
@@ -69,13 +69,19 @@ module Generators
69
69
  class Migrator
70
70
  class Error < RuntimeError; end
71
71
 
72
- @ignore_models = []
73
- @ignore_tables = []
72
+ DEFAULT_CHARSET = :utf8mb4
73
+ DEFAULT_COLLATION = :utf8mb4_general
74
+
75
+ @ignore_models = []
76
+ @ignore_tables = []
74
77
  @before_generating_migration_callback = nil
75
- @active_record_class = ActiveRecord::Base
78
+ @active_record_class = ActiveRecord::Base
79
+ @default_charset = DEFAULT_CHARSET
80
+ @default_collation = DEFAULT_COLLATION
76
81
 
77
82
  class << self
78
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints, :active_record_class
83
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints,
84
+ :active_record_class, :default_charset, :default_collation
79
85
  attr_reader :before_generating_migration_callback
80
86
 
81
87
  def active_record_class
@@ -292,56 +298,80 @@ module Generators
292
298
  "drop_table :#{t}"
293
299
  end * "\n"
294
300
 
295
- changes = []
296
- undo_changes = []
297
- index_changes = []
298
- undo_index_changes = []
299
- fk_changes = []
300
- undo_fk_changes = []
301
+ changes = []
302
+ undo_changes = []
303
+ index_changes = []
304
+ undo_index_changes = []
305
+ fk_changes = []
306
+ undo_fk_changes = []
307
+ table_options_changes = []
308
+ undo_table_options_changes = []
309
+
301
310
  to_change.each do |t|
302
311
  model = models_by_table_name[t]
303
312
  table = to_rename.key(t) || model.table_name
304
313
  if table.in?(db_tables)
305
- change, undo, index_change, undo_index, fk_change, undo_fk = change_table(model, table)
314
+ change, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
306
315
  changes << change
307
316
  undo_changes << undo
308
317
  index_changes << index_change
309
318
  undo_index_changes << undo_index
310
319
  fk_changes << fk_change
311
320
  undo_fk_changes << undo_fk
321
+ table_options_changes << table_options_change
322
+ undo_table_options_changes << undo_table_options_change
312
323
  end
313
324
  end
314
325
 
315
- up = [renames, drops, creates, changes, index_changes, fk_changes].flatten.reject(&:blank?) * "\n\n"
316
- down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
326
+ up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
327
+ down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes, undo_table_options_changes].flatten.reject(&:blank?) * "\n\n"
317
328
 
318
329
  [up, down]
319
330
  end
320
331
 
321
332
  def create_table(model)
322
- longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
323
- disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
324
- primary_key_option =
325
- if model.primary_key.blank? || disable_auto_increment
326
- ", id: false"
327
- elsif model.primary_key == "id"
328
- ", id: :bigint"
329
- else
330
- ", primary_key: :#{model.primary_key}"
333
+ longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
334
+ disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
335
+ table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
336
+ field_definitions = [
337
+ disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
338
+ *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
339
+ ].compact
340
+
341
+ <<~EOS.strip
342
+ create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
343
+ #{field_definitions.join("\n")}
331
344
  end
332
- (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
333
- [(disable_auto_increment ? " t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil)] +
334
- model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) } +
335
- ["end"] + (if Migrator.disable_indexing
336
- []
337
- else
338
- create_indexes(model) +
339
- create_constraints(model)
340
- end)).compact * "\n"
345
+
346
+ #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
347
+ #{create_indexes(model).join("\n") unless Migrator.disable_indexing}
348
+ #{create_constraints(model).join("\n") unless Migrator.disable_indexing}
349
+ EOS
350
+ end
351
+
352
+ def create_table_options(model, disable_auto_increment)
353
+ if model.primary_key.blank? || disable_auto_increment
354
+ "id: false"
355
+ elsif model.primary_key == "id"
356
+ "id: :bigint"
357
+ else
358
+ "primary_key: :#{model.primary_key}"
359
+ end
360
+ end
361
+
362
+ def table_options_for_model(model)
363
+ if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
364
+ {}
365
+ else
366
+ {
367
+ charset: model.table_options[:charset] || Migrator.default_charset,
368
+ collation: model.table_options[:collation] || Migrator.default_collation
369
+ }
370
+ end
341
371
  end
342
372
 
343
373
  def create_indexes(model)
344
- model.index_specs.map { |i| i.to_add_statement(model.table_name) }
374
+ model.index_definitions.map { |i| i.to_add_statement(model.table_name) }
345
375
  end
346
376
 
347
377
  def create_constraints(model)
@@ -351,7 +381,7 @@ module Generators
351
381
  def create_field(field_spec, field_name_width)
352
382
  options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
353
383
  args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
354
- format(" t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
384
+ format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
355
385
  end
356
386
 
357
387
  def change_table(model, current_table_name)
@@ -413,15 +443,17 @@ module Generators
413
443
  col_name = old_names[c] || c
414
444
  col = db_columns[col_name]
415
445
  spec = model.field_specs[c]
416
- if spec.different_to?(col) # TODO: DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
446
+ 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
417
447
  change_spec = fk_field_options(model, c)
418
448
  change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
419
449
  ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
420
450
  (spec.limit || col.limit)
421
- change_spec[:precision] = spec.precision unless spec.precision.nil?
422
- change_spec[:scale] = spec.scale unless spec.scale.nil?
423
- change_spec[:null] = spec.null unless spec.null && col.null
424
- change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
451
+ change_spec[:precision] = spec.precision unless spec.precision.nil?
452
+ change_spec[:scale] = spec.scale unless spec.scale.nil?
453
+ change_spec[:null] = spec.null unless spec.null && col.null
454
+ change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
455
+ change_spec[:collation] = spec.collation unless spec.collation.nil?
456
+ change_spec[:charset] = spec.charset unless spec.charset.nil?
425
457
 
426
458
  changes << "change_column :#{new_table_name}, :#{c}, " +
427
459
  ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
@@ -436,21 +468,28 @@ module Generators
436
468
  else
437
469
  change_foreign_key_constraints(model, current_table_name)
438
470
  end
471
+ table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
472
+ change_table_options(model, current_table_name)
473
+ else
474
+ [[], []]
475
+ end
439
476
 
440
477
  [(renames + adds + removes + changes) * "\n",
441
478
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
442
479
  index_changes * "\n",
443
480
  undo_index_changes * "\n",
444
481
  fk_changes * "\n",
445
- undo_fk_changes * "\n"]
482
+ undo_fk_changes * "\n",
483
+ table_options_changes * "\n",
484
+ undo_table_options_changes * "\n"]
446
485
  end
447
486
 
448
487
  def change_indexes(model, old_table_name)
449
488
  return [[], []] if Migrator.disable_constraints
450
489
 
451
490
  new_table_name = model.table_name
452
- existing_indexes = ::DeclareSchema::Model::IndexSpec.for_model(model, old_table_name)
453
- model_indexes_with_equivalents = model.index_specs_with_primary_key
491
+ existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
492
+ model_indexes_with_equivalents = model.index_definitions_with_primary_key
454
493
  model_indexes = model_indexes_with_equivalents.map do |i|
455
494
  if i.explicit_name.nil?
456
495
  if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
@@ -458,20 +497,20 @@ module Generators
458
497
  end
459
498
  end || i
460
499
  end
461
- existing_has_primary_key = existing_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
462
- model_has_primary_key = model_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
500
+ existing_has_primary_key = existing_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
501
+ model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
463
502
 
464
503
  add_indexes_init = model_indexes - existing_indexes
465
504
  drop_indexes_init = existing_indexes - model_indexes
466
505
  undo_add_indexes = []
467
506
  undo_drop_indexes = []
468
507
  add_indexes = add_indexes_init.map do |i|
469
- undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == "PRIMARY_KEY"
508
+ undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
470
509
  i.to_add_statement(new_table_name, existing_has_primary_key)
471
510
  end
472
511
  drop_indexes = drop_indexes_init.map do |i|
473
512
  undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
474
- drop_index(new_table_name, i.name) unless i.name == "PRIMARY_KEY"
513
+ drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
475
514
  end.compact
476
515
 
477
516
  # the order is important here - adding a :unique, for instance needs to remove then add
@@ -489,7 +528,7 @@ module Generators
489
528
  return [[], []] if Migrator.disable_indexing
490
529
 
491
530
  new_table_name = model.table_name
492
- existing_fks = ::DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
531
+ existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
493
532
  model_fks = model.constraint_specs
494
533
  add_fks = model_fks - existing_fks
495
534
  drop_fks = existing_fks - model_fks
@@ -552,6 +591,20 @@ module Generators
552
591
  end
553
592
  end
554
593
 
594
+ def change_table_options(model, current_table_name)
595
+ old_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.for_model(model, current_table_name)
596
+ new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
597
+
598
+ if old_options_definition.equivalent?(new_options_definition)
599
+ [[], []]
600
+ else
601
+ [
602
+ [new_options_definition.alter_table_statement],
603
+ [old_options_definition.alter_table_statement]
604
+ ]
605
+ end
606
+ end
607
+
555
608
  def revert_table(table)
556
609
  res = StringIO.new
557
610
  schema_dumper_klass = case Rails::VERSION::MAJOR