declare_schema 0.1.0

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.dependabot/config.yml +10 -0
  3. data/.github/workflows/gem_release.yml +38 -0
  4. data/.gitignore +14 -0
  5. data/.jenkins/Jenkinsfile +72 -0
  6. data/.jenkins/ruby_build_pod.yml +19 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +189 -0
  9. data/.ruby-version +1 -0
  10. data/Appraisals +14 -0
  11. data/CHANGELOG.md +11 -0
  12. data/Gemfile +24 -0
  13. data/Gemfile.lock +203 -0
  14. data/LICENSE.txt +22 -0
  15. data/README.md +11 -0
  16. data/Rakefile +56 -0
  17. data/bin/declare_schema +11 -0
  18. data/declare_schema.gemspec +25 -0
  19. data/gemfiles/.bundle/config +2 -0
  20. data/gemfiles/rails_4.gemfile +25 -0
  21. data/gemfiles/rails_5.gemfile +25 -0
  22. data/gemfiles/rails_6.gemfile +25 -0
  23. data/lib/declare_schema.rb +44 -0
  24. data/lib/declare_schema/command.rb +65 -0
  25. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
  26. data/lib/declare_schema/extensions/module.rb +36 -0
  27. data/lib/declare_schema/field_declaration_dsl.rb +40 -0
  28. data/lib/declare_schema/model.rb +242 -0
  29. data/lib/declare_schema/model/field_spec.rb +162 -0
  30. data/lib/declare_schema/model/index_spec.rb +175 -0
  31. data/lib/declare_schema/railtie.rb +12 -0
  32. data/lib/declare_schema/version.rb +5 -0
  33. data/lib/generators/declare_schema/migration/USAGE +47 -0
  34. data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
  35. data/lib/generators/declare_schema/migration/migrator.rb +567 -0
  36. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
  37. data/lib/generators/declare_schema/model/USAGE +19 -0
  38. data/lib/generators/declare_schema/model/model_generator.rb +12 -0
  39. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
  40. data/lib/generators/declare_schema/support/eval_template.rb +21 -0
  41. data/lib/generators/declare_schema/support/model.rb +64 -0
  42. data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
  43. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
  44. data/spec/spec_helper.rb +28 -0
  45. data/test/api.rdoctest +136 -0
  46. data/test/doc-only.rdoctest +76 -0
  47. data/test/generators.rdoctest +60 -0
  48. data/test/interactive_primary_key.rdoctest +56 -0
  49. data/test/migration_generator.rdoctest +846 -0
  50. data/test/migration_generator_comments.rdoctestDISABLED +74 -0
  51. data/test/prepare_testapp.rb +15 -0
  52. data/test_responses.txt +2 -0
  53. metadata +109 -0
@@ -0,0 +1,567 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Generators
6
+ module DeclareSchema
7
+ module Migration
8
+ HabtmModelShim = Struct.new(:join_table, :foreign_keys, :foreign_key_classes, :connection) do
9
+ class << self
10
+ def from_reflection(refl)
11
+ join_table = refl.join_table
12
+ foreign_keys_and_classes = [
13
+ [refl.foreign_key.to_s, refl.active_record],
14
+ [refl.association_foreign_key.to_s, refl.class_name.constantize]
15
+ ].sort { |a, b| a.first <=> b.first }
16
+ foreign_keys = foreign_keys_and_classes.map(&:first)
17
+ foreign_key_classes = foreign_keys_and_classes.map(&:last)
18
+ # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
19
+ # figure that anybody who sets THAT up can deal with their own migrations...
20
+ connection = refl.active_record.connection
21
+
22
+ new(join_table, foreign_keys, foreign_key_classes, connection)
23
+ end
24
+ end
25
+
26
+ def table_name
27
+ join_table
28
+ end
29
+
30
+ def table_exists?
31
+ ActiveRecord::Migration.table_exists? table_name
32
+ end
33
+
34
+ def field_specs
35
+ i = 0
36
+ foreign_keys.reduce({}) do |h, v|
37
+ # some trickery to avoid an infinite loop when FieldSpec#initialize tries to call model.field_specs
38
+ h[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :integer, position: i, null: false)
39
+ i += 1
40
+ h
41
+ end
42
+ end
43
+
44
+ def primary_key
45
+ false # no single-column primary key
46
+ end
47
+
48
+ def index_specs_with_primary_key
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
52
+ ]
53
+ end
54
+
55
+ alias_method :index_specs, :index_specs_with_primary_key
56
+
57
+ def ignore_indexes
58
+ []
59
+ end
60
+
61
+ def constraint_specs
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)
65
+ ]
66
+ end
67
+ end
68
+
69
+ class Migrator
70
+ class Error < RuntimeError; end
71
+
72
+ @ignore_models = []
73
+ @ignore_tables = []
74
+ @active_record_class = ActiveRecord::Base
75
+
76
+ class << self
77
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints, :active_record_class
78
+
79
+ def active_record_class
80
+ @active_record_class.is_a?(Class) or @active_record_class = @active_record_class.to_s.constantize
81
+ @active_record_class
82
+ end
83
+
84
+ def run(renames = {})
85
+ g = Migrator.new
86
+ g.renames = renames
87
+ g.generate
88
+ end
89
+
90
+ def default_migration_name
91
+ existing = Dir["#{Rails.root}/db/migrate/*declare_schema_migration*"]
92
+ max = existing.grep(/([0-9]+)\.rb$/) { Regexp.last_match(1).to_i }.max.to_i
93
+ "declare_schema_migration_#{max + 1}"
94
+ end
95
+
96
+ def connection
97
+ ActiveRecord::Base.connection
98
+ end
99
+ end
100
+
101
+ def initialize(ambiguity_resolver = {})
102
+ @ambiguity_resolver = ambiguity_resolver
103
+ @drops = []
104
+ @renames = nil
105
+ end
106
+
107
+ attr_accessor :renames
108
+
109
+ def load_rails_models
110
+ Rails.application.eager_load!
111
+ end
112
+
113
+ # Returns an array of model classes that *directly* extend
114
+ # ActiveRecord::Base, excluding anything in the CGI module
115
+ def table_model_classes
116
+ load_rails_models
117
+ ActiveRecord::Base.send(:descendants).reject { |c| (c.base_class != c) || c.name.starts_with?("CGI::") }
118
+ end
119
+
120
+ def connection
121
+ self.class.connection
122
+ end
123
+
124
+ class << self
125
+ def fix_native_types(types)
126
+ case connection.class.name
127
+ when /mysql/i
128
+ types[:integer][:limit] ||= 11
129
+ types[:text][:limit] ||= 0xffff
130
+ types[:binary][:limit] ||= 0xffff
131
+ end
132
+ types
133
+ end
134
+
135
+ def native_types
136
+ @native_types ||= fix_native_types(connection.native_database_types)
137
+ end
138
+ end
139
+
140
+ def native_types
141
+ self.class.native_types
142
+ end
143
+
144
+ # list habtm join tables
145
+ def habtm_tables
146
+ reflections = Hash.new { |h, k| h[k] = [] }
147
+ ActiveRecord::Base.send(:descendants).map do |c|
148
+ c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
149
+ reflections[a.join_table] << a
150
+ end
151
+ end
152
+ reflections
153
+ end
154
+
155
+ # Returns an array of model classes and an array of table names
156
+ # that generation needs to take into account
157
+ def models_and_tables
158
+ ignore_model_names = Migrator.ignore_models.map { |model| model.to_s.underscore }
159
+ all_models = table_model_classes
160
+ declare_schema_models = all_models.select do |m|
161
+ (m.name['HABTM_'] ||
162
+ (m.include_in_migration if m.respond_to?(:include_in_migration))) && !m.name.underscore.in?(ignore_model_names)
163
+ end
164
+ non_declare_schema_models = all_models - declare_schema_models
165
+ db_tables = connection.tables - Migrator.ignore_tables.map(&:to_s) - non_declare_schema_models.map(&:table_name)
166
+ [declare_schema_models, db_tables]
167
+ end
168
+
169
+ # return a hash of table renames and modifies the passed arrays so
170
+ # that renamed tables are no longer listed as to_create or to_drop
171
+ def extract_table_renames!(to_create, to_drop)
172
+ if renames
173
+ # A hash of table renames has been provided
174
+
175
+ to_rename = {}
176
+ renames.each_pair do |old_name, new_name|
177
+ new_name = new_name[:table_name] if new_name.is_a?(Hash)
178
+ next unless new_name
179
+
180
+ if to_create.delete(new_name.to_s) && to_drop.delete(old_name.to_s)
181
+ to_rename[old_name.to_s] = new_name.to_s
182
+ else
183
+ raise Error, "Invalid table rename specified: #{old_name} => #{new_name}"
184
+ end
185
+ end
186
+ to_rename
187
+
188
+ elsif @ambiguity_resolver
189
+ @ambiguity_resolver.call(to_create, to_drop, "table", nil)
190
+
191
+ else
192
+ raise Error, "Unable to resolve migration ambiguities"
193
+ end
194
+ end
195
+
196
+ def extract_column_renames!(to_add, to_remove, table_name)
197
+ if renames
198
+ to_rename = {}
199
+ if (column_renames = renames&.[](table_name.to_sym))
200
+ # A hash of table renames has been provided
201
+
202
+ column_renames.each_pair do |old_name, new_name|
203
+ if to_add.delete(new_name.to_s) && to_remove.delete(old_name.to_s)
204
+ to_rename[old_name.to_s] = new_name.to_s
205
+ else
206
+ raise Error, "Invalid rename specified: #{old_name} => #{new_name}"
207
+ end
208
+ end
209
+ end
210
+ to_rename
211
+
212
+ elsif @ambiguity_resolver
213
+ @ambiguity_resolver.call(to_add, to_remove, "column", "#{table_name}.")
214
+
215
+ else
216
+ raise Error, "Unable to resolve migration ambiguities in table #{table_name}"
217
+ end
218
+ end
219
+
220
+ def always_ignore_tables
221
+ # TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
222
+ if defined?(CGI::Session::ActiveRecordStore::Session) &&
223
+ defined?(ActionController::Base) &&
224
+ ActionController::Base.session_store == CGI::Session::ActiveRecordStore
225
+ sessions_table = CGI::Session::ActiveRecordStore::Session.table_name
226
+ end
227
+ ['schema_info', 'schema_migrations', 'simple_sesions', sessions_table].compact
228
+ end
229
+
230
+ def generate
231
+ models, db_tables = models_and_tables
232
+ models_by_table_name = {}
233
+ models.each do |m|
234
+ if !models_by_table_name.has_key?(m.table_name)
235
+ models_by_table_name[m.table_name] = m
236
+ elsif m.superclass == models_by_table_name[m.table_name].superclass.superclass
237
+ # we need to ensure that models_by_table_name contains the
238
+ # base class in an STI hierarchy
239
+ models_by_table_name[m.table_name] = m
240
+ end
241
+ end
242
+ # generate shims for HABTM models
243
+ habtm_tables.each do |name, refls|
244
+ models_by_table_name[name] = HabtmModelShim.from_reflection(refls.first)
245
+ end
246
+ model_table_names = models_by_table_name.keys
247
+
248
+ to_create = model_table_names - db_tables
249
+ to_drop = db_tables - model_table_names - always_ignore_tables
250
+ to_change = model_table_names
251
+ to_rename = extract_table_renames!(to_create, to_drop)
252
+
253
+ renames = to_rename.map do |old_name, new_name|
254
+ "rename_table :#{old_name}, :#{new_name}"
255
+ end * "\n"
256
+ undo_renames = to_rename.map do |old_name, new_name|
257
+ "rename_table :#{new_name}, :#{old_name}"
258
+ end * "\n"
259
+
260
+ drops = to_drop.map do |t|
261
+ "drop_table :#{t}"
262
+ end * "\n"
263
+ undo_drops = to_drop.map do |t|
264
+ revert_table(t)
265
+ end * "\n\n"
266
+
267
+ creates = to_create.map do |t|
268
+ create_table(models_by_table_name[t])
269
+ end * "\n\n"
270
+ undo_creates = to_create.map do |t|
271
+ "drop_table :#{t}"
272
+ end * "\n"
273
+
274
+ changes = []
275
+ undo_changes = []
276
+ index_changes = []
277
+ undo_index_changes = []
278
+ fk_changes = []
279
+ undo_fk_changes = []
280
+ to_change.each do |t|
281
+ model = models_by_table_name[t]
282
+ table = to_rename.key(t) || model.table_name
283
+ if table.in?(db_tables)
284
+ change, undo, index_change, undo_index, fk_change, undo_fk = change_table(model, table)
285
+ changes << change
286
+ undo_changes << undo
287
+ index_changes << index_change
288
+ undo_index_changes << undo_index
289
+ fk_changes << fk_change
290
+ undo_fk_changes << undo_fk
291
+ end
292
+ end
293
+
294
+ up = [renames, drops, creates, changes, index_changes, fk_changes].flatten.reject(&:blank?) * "\n\n"
295
+ down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
296
+
297
+ [up, down]
298
+ rescue Exception => ex
299
+ puts "Caught exception: #{ex}"
300
+ puts ex.backtrace.join("\n")
301
+ raise
302
+ end
303
+
304
+ def create_table(model)
305
+ longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
306
+ disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
307
+ primary_key_option =
308
+ if model.primary_key.blank? || disable_auto_increment
309
+ ", id: false"
310
+ elsif model.primary_key == "id"
311
+ ", id: :bigint"
312
+ else
313
+ ", primary_key: :#{model.primary_key}"
314
+ end
315
+ (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
316
+ [(disable_auto_increment ? " t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil)] +
317
+ model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) } +
318
+ ["end"] + (if Migrator.disable_indexing
319
+ []
320
+ else
321
+ create_indexes(model) +
322
+ create_constraints(model)
323
+ end)).compact * "\n"
324
+ end
325
+
326
+ def create_indexes(model)
327
+ model.index_specs.map { |i| i.to_add_statement(model.table_name) }
328
+ end
329
+
330
+ def create_constraints(model)
331
+ model.constraint_specs.map { |fk| fk.to_add_statement(model.table_name) }
332
+ end
333
+
334
+ def create_field(field_spec, field_name_width)
335
+ options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
336
+ args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
337
+ format(" t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
338
+ end
339
+
340
+ def change_table(model, current_table_name)
341
+ new_table_name = model.table_name
342
+
343
+ db_columns = model.connection.columns(current_table_name).index_by(&:name)
344
+ key_missing = db_columns[model.primary_key].nil? && model.primary_key.present?
345
+ if model.primary_key.present?
346
+ db_columns.delete(model.primary_key)
347
+ end
348
+
349
+ model_column_names = model.field_specs.keys.map(&:to_s)
350
+ db_column_names = db_columns.keys.map(&:to_s)
351
+
352
+ to_add = model_column_names - db_column_names
353
+ to_add += [model.primary_key] if key_missing && model.primary_key.present?
354
+ to_remove = db_column_names - model_column_names
355
+ to_remove -= [model.primary_key.to_sym] if model.primary_key.present?
356
+
357
+ to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
358
+
359
+ db_column_names -= to_rename.keys
360
+ db_column_names |= to_rename.values
361
+ to_change = db_column_names & model_column_names
362
+
363
+ renames = to_rename.map do |old_name, new_name|
364
+ "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
365
+ end
366
+ undo_renames = to_rename.map do |old_name, new_name|
367
+ "rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
368
+ end
369
+
370
+ to_add = to_add.sort_by { |c| model.field_specs[c]&.position || 0 }
371
+ adds = to_add.map do |c|
372
+ args =
373
+ if (spec = model.field_specs[c])
374
+ options = fk_field_options(model, c).merge(spec.sql_options)
375
+ [":#{spec.sql_type}", *format_options(options, spec.sql_type)]
376
+ else
377
+ [":integer"]
378
+ end
379
+ "add_column :#{new_table_name}, :#{c}, #{args.join(', ')}"
380
+ end
381
+ undo_adds = to_add.map do |c|
382
+ "remove_column :#{new_table_name}, :#{c}"
383
+ end
384
+
385
+ removes = to_remove.map do |c|
386
+ "remove_column :#{new_table_name}, :#{c}"
387
+ end
388
+ undo_removes = to_remove.map do |c|
389
+ revert_column(current_table_name, c)
390
+ end
391
+
392
+ old_names = to_rename.invert
393
+ changes = []
394
+ undo_changes = []
395
+ to_change.each do |c|
396
+ col_name = old_names[c] || c
397
+ col = db_columns[col_name]
398
+ spec = model.field_specs[c]
399
+ 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
400
+ change_spec = fk_field_options(model, c)
401
+ change_spec[:limit] = spec.limit if (spec.sql_type != :text ||
402
+ ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
403
+ (spec.limit || col.limit)
404
+ change_spec[:precision] = spec.precision unless spec.precision.nil?
405
+ change_spec[:scale] = spec.scale unless spec.scale.nil?
406
+ change_spec[:null] = spec.null unless spec.null && col.null
407
+ change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
408
+ change_spec[:comment] = spec.comment unless spec.comment.nil? && (col.comment if col.respond_to?(:comment)).nil?
409
+
410
+ changes << "change_column :#{new_table_name}, :#{c}, " +
411
+ ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
412
+ back = change_column_back(current_table_name, col_name)
413
+ undo_changes << back unless back.blank?
414
+ end
415
+ end.compact
416
+
417
+ index_changes, undo_index_changes = change_indexes(model, current_table_name)
418
+ fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
419
+ [[], []]
420
+ else
421
+ change_foreign_key_constraints(model, current_table_name)
422
+ end
423
+
424
+ [(renames + adds + removes + changes) * "\n",
425
+ (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
426
+ index_changes * "\n",
427
+ undo_index_changes * "\n",
428
+ fk_changes * "\n",
429
+ undo_fk_changes * "\n"]
430
+ end
431
+
432
+ def change_indexes(model, old_table_name)
433
+ return [[], []] if Migrator.disable_constraints
434
+
435
+ new_table_name = model.table_name
436
+ existing_indexes = ::DeclareSchema::Model::IndexSpec.for_model(model, old_table_name)
437
+ model_indexes_with_equivalents = model.index_specs_with_primary_key
438
+ model_indexes = model_indexes_with_equivalents.map do |i|
439
+ if i.explicit_name.nil?
440
+ if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
441
+ i.with_name(ex.name)
442
+ end
443
+ end || i
444
+ end
445
+ existing_has_primary_key = existing_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
446
+ model_has_primary_key = model_indexes.any? { |i| i.name == 'PRIMARY_KEY' }
447
+
448
+ add_indexes_init = model_indexes - existing_indexes
449
+ drop_indexes_init = existing_indexes - model_indexes
450
+ undo_add_indexes = []
451
+ undo_drop_indexes = []
452
+ add_indexes = add_indexes_init.map do |i|
453
+ undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == "PRIMARY_KEY"
454
+ i.to_add_statement(new_table_name, existing_has_primary_key)
455
+ end
456
+ drop_indexes = drop_indexes_init.map do |i|
457
+ undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
458
+ drop_index(new_table_name, i.name) unless i.name == "PRIMARY_KEY"
459
+ end.compact
460
+
461
+ # the order is important here - adding a :unique, for instance needs to remove then add
462
+ [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
463
+ end
464
+
465
+ def drop_index(table, name)
466
+ # see https://hobo.lighthouseapp.com/projects/8324/tickets/566
467
+ # for why the rescue exists
468
+ "remove_index :#{table}, name: :#{name} rescue ActiveRecord::StatementInvalid"
469
+ end
470
+
471
+ def change_foreign_key_constraints(model, old_table_name)
472
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise 'SQLite does not support foreign keys'
473
+ return [[], []] if Migrator.disable_indexing
474
+
475
+ new_table_name = model.table_name
476
+ existing_fks = DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
477
+ model_fks = model.constraint_specs
478
+ add_fks = model_fks - existing_fks
479
+ drop_fks = existing_fks - model_fks
480
+ undo_add_fks = []
481
+ undo_drop_fks = []
482
+
483
+ add_fks.map! do |fk|
484
+ # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
485
+ undo_add_fks << remove_foreign_key(old_table_name, fk.options[:constraint_name])
486
+ fk.to_add_statement
487
+ end.compact
488
+
489
+ drop_fks.map! do |fk|
490
+ undo_drop_fks << fk.to_add_statement
491
+ remove_foreign_key(new_table_name, fk.options[:constraint_name])
492
+ end
493
+
494
+ [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
495
+ end
496
+
497
+ def remove_foreign_key(old_table_name, fk_name)
498
+ "remove_foreign_key('#{old_table_name}', name: '#{fk_name}')"
499
+ end
500
+
501
+ def format_options(options, type, changing: false)
502
+ options.map do |k, v|
503
+ unless changing
504
+ next if k == :limit && (type == :decimal || v == native_types[type][:limit])
505
+ next if k == :null && v == true
506
+ end
507
+
508
+ next if k == :limit && type == :text &&
509
+ (!::DeclareSchema::Model::FieldSpec.mysql_text_limits? || v == ::DeclareSchema::Model::FieldSpec::MYSQL_LONGTEXT_LIMIT)
510
+
511
+ if k.is_a?(Symbol)
512
+ "#{k}: #{v.inspect}"
513
+ else
514
+ "#{k.inspect} => #{v.inspect}"
515
+ end
516
+ end.compact
517
+ end
518
+
519
+ def fk_field_options(model, field_name)
520
+ if (foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }) && (parent_table = foreign_key.parent_table_name)
521
+ parent_columns = connection.columns(parent_table) rescue []
522
+ pk_column = parent_columns.find { |column| column.name == "id" } # right now foreign keys assume id is the target
523
+ pk_limit = pk_column ? pk_column.cast_type.limit : 8
524
+ { limit: pk_limit }
525
+ else
526
+ {}
527
+ end
528
+ end
529
+
530
+ def revert_table(table)
531
+ res = StringIO.new
532
+ schema_dumper_klass = case Rails::VERSION::MAJOR
533
+ when 4
534
+ ActiveRecord::SchemaDumper
535
+ else
536
+ ActiveRecord::ConnectionAdapters::SchemaDumper
537
+ end
538
+ schema_dumper_klass.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
539
+ res.string.strip.gsub("\n ", "\n")
540
+ end
541
+
542
+ def column_options_from_reverted_table(table, col_name)
543
+ revert = revert_table(table)
544
+ if (md = revert.match(/\s*t\.column\s+"#{col_name}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
545
+ # Ugly migration
546
+ _, type, options = *md
547
+ elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{col_name}"(?:,\s+(.*?)$)?/m))
548
+ # Sexy migration
549
+ _, type, options = *md
550
+ type = ":#{type}"
551
+ end
552
+ [type, options]
553
+ end
554
+
555
+ def change_column_back(table, col_name)
556
+ type, options = column_options_from_reverted_table(table, col_name)
557
+ "change_column :#{table}, :#{col_name}, #{type}#{', ' + options.strip if options}"
558
+ end
559
+
560
+ def revert_column(table, column)
561
+ type, options = column_options_from_reverted_table(table, column)
562
+ "add_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
563
+ end
564
+ end
565
+ end
566
+ end
567
+ end