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