declare_schema 0.1.0

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