declare_schema 0.3.0 → 0.5.0.pre.1

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