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.
- checksums.yaml +7 -0
- data/.dependabot/config.yml +10 -0
- data/.github/workflows/gem_release.yml +38 -0
- data/.gitignore +14 -0
- data/.jenkins/Jenkinsfile +72 -0
- data/.jenkins/ruby_build_pod.yml +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +189 -0
- data/.ruby-version +1 -0
- data/Appraisals +14 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +203 -0
- data/LICENSE.txt +22 -0
- data/README.md +11 -0
- data/Rakefile +56 -0
- data/bin/declare_schema +11 -0
- data/declare_schema.gemspec +25 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_4.gemfile +25 -0
- data/gemfiles/rails_5.gemfile +25 -0
- data/gemfiles/rails_6.gemfile +25 -0
- data/lib/declare_schema.rb +44 -0
- data/lib/declare_schema/command.rb +65 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
- data/lib/declare_schema/extensions/module.rb +36 -0
- data/lib/declare_schema/field_declaration_dsl.rb +40 -0
- data/lib/declare_schema/model.rb +242 -0
- data/lib/declare_schema/model/field_spec.rb +162 -0
- data/lib/declare_schema/model/index_spec.rb +175 -0
- data/lib/declare_schema/railtie.rb +12 -0
- data/lib/declare_schema/version.rb +5 -0
- data/lib/generators/declare_schema/migration/USAGE +47 -0
- data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
- data/lib/generators/declare_schema/migration/migrator.rb +567 -0
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/declare_schema/model/USAGE +19 -0
- data/lib/generators/declare_schema/model/model_generator.rb +12 -0
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
- data/lib/generators/declare_schema/support/eval_template.rb +21 -0
- data/lib/generators/declare_schema/support/model.rb +64 -0
- data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- data/test/api.rdoctest +136 -0
- data/test/doc-only.rdoctest +76 -0
- data/test/generators.rdoctest +60 -0
- data/test/interactive_primary_key.rdoctest +56 -0
- data/test/migration_generator.rdoctest +846 -0
- data/test/migration_generator_comments.rdoctestDISABLED +74 -0
- data/test/prepare_testapp.rb +15 -0
- data/test_responses.txt +2 -0
- 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
|