online_migrations 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/.github/workflows/test.yml +112 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +113 -0
- data/.yardopts +1 -0
- data/BACKGROUND_MIGRATIONS.md +288 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +1067 -0
- data/Rakefile +23 -0
- data/gemfiles/activerecord_42.gemfile +6 -0
- data/gemfiles/activerecord_50.gemfile +5 -0
- data/gemfiles/activerecord_51.gemfile +5 -0
- data/gemfiles/activerecord_52.gemfile +5 -0
- data/gemfiles/activerecord_60.gemfile +5 -0
- data/gemfiles/activerecord_61.gemfile +5 -0
- data/gemfiles/activerecord_70.gemfile +5 -0
- data/gemfiles/activerecord_head.gemfile +5 -0
- data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
- data/lib/generators/online_migrations/install_generator.rb +34 -0
- data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
- data/lib/online_migrations/background_migration.rb +64 -0
- data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
- data/lib/online_migrations/background_migrations/config.rb +98 -0
- data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
- data/lib/online_migrations/background_migrations/migration.rb +210 -0
- data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
- data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
- data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
- data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
- data/lib/online_migrations/batch_iterator.rb +87 -0
- data/lib/online_migrations/change_column_type_helpers.rb +587 -0
- data/lib/online_migrations/command_checker.rb +590 -0
- data/lib/online_migrations/command_recorder.rb +137 -0
- data/lib/online_migrations/config.rb +198 -0
- data/lib/online_migrations/copy_trigger.rb +91 -0
- data/lib/online_migrations/database_tasks.rb +19 -0
- data/lib/online_migrations/error_messages.rb +388 -0
- data/lib/online_migrations/foreign_key_definition.rb +17 -0
- data/lib/online_migrations/foreign_keys_collector.rb +33 -0
- data/lib/online_migrations/indexes_collector.rb +48 -0
- data/lib/online_migrations/lock_retrier.rb +250 -0
- data/lib/online_migrations/migration.rb +63 -0
- data/lib/online_migrations/migrator.rb +23 -0
- data/lib/online_migrations/schema_cache.rb +96 -0
- data/lib/online_migrations/schema_statements.rb +1042 -0
- data/lib/online_migrations/utils.rb +140 -0
- data/lib/online_migrations/version.rb +5 -0
- data/lib/online_migrations.rb +74 -0
- data/online_migrations.gemspec +28 -0
- metadata +119 -0
@@ -0,0 +1,1042 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module SchemaStatements
|
5
|
+
include ChangeColumnTypeHelpers
|
6
|
+
include BackgroundMigrations::MigrationHelpers
|
7
|
+
|
8
|
+
# Updates the value of a column in batches.
|
9
|
+
#
|
10
|
+
# @param table_name [String, Symbol]
|
11
|
+
# @param column_name [String, Symbol]
|
12
|
+
# @param value value for the column. It is typically a literal. To perform a computed
|
13
|
+
# update, an Arel literal can be used instead
|
14
|
+
# @option options [Integer] :batch_size (1000) size of the batch
|
15
|
+
# @option options [String, Symbol] :batch_column_name (primary key) option is for tables without primary key, in this
|
16
|
+
# case another unique integer column can be used. Example: `:user_id`
|
17
|
+
# @option options [Proc, Boolean] :progress (false) whether to show progress while running.
|
18
|
+
# - when `true` - will show progress (prints "." for each batch)
|
19
|
+
# - when `false` - will not show progress
|
20
|
+
# - when `Proc` - will call the proc on each iteration with the batched relation as argument.
|
21
|
+
# Example: `proc { |_relation| print "." }`
|
22
|
+
# @option options [Integer] :pause_ms (50) The number of milliseconds to sleep between each batch execution.
|
23
|
+
# This helps to reduce database pressure while running updates and gives time to do maintenance tasks
|
24
|
+
#
|
25
|
+
# @yield [relation] a block to be called to add extra conditions to the queries being executed
|
26
|
+
# @yieldparam relation [ActiveRecord::Relation] an instance of `ActiveRecord::Relation`
|
27
|
+
# to add extra conditions to
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# update_column_in_batches(:users, :admin, false)
|
33
|
+
#
|
34
|
+
# @example With extra conditions
|
35
|
+
# update_column_in_batches(:users, :name, "Guest") do |relation|
|
36
|
+
# relation.where(name: nil)
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example From other column
|
40
|
+
# update_column_in_batches(:users, :name_for_type_change, Arel.sql("name"))
|
41
|
+
#
|
42
|
+
# @example With computed value
|
43
|
+
# truncated_name = Arel.sql("substring(name from 1 for 64)")
|
44
|
+
# update_column_in_batches(:users, :name, truncated_name) do |relation|
|
45
|
+
# relation.where("length(name) > 64")
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# @note This method should not be run within a transaction
|
49
|
+
# @note Consider `update_columns_in_batches` when updating multiple columns
|
50
|
+
# to avoid rewriting the table multiple times.
|
51
|
+
# @note For extra large tables (100s of millions of records)
|
52
|
+
# you may consider using `backfill_column_in_background` or `copy_column_in_background`.
|
53
|
+
#
|
54
|
+
def update_column_in_batches(table_name, column_name, value, **options, &block)
|
55
|
+
update_columns_in_batches(table_name, [[column_name, value]], **options, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Same as `update_column_in_batches`, but for multiple columns.
|
59
|
+
#
|
60
|
+
# This is useful to avoid multiple costly disk rewrites of large tables
|
61
|
+
# when updating each column separately.
|
62
|
+
#
|
63
|
+
# @param columns_and_values
|
64
|
+
# columns_and_values is an array of arrays (first item is a column name, second - new value)
|
65
|
+
#
|
66
|
+
# @see #update_column_in_batches
|
67
|
+
#
|
68
|
+
def update_columns_in_batches(table_name, columns_and_values,
|
69
|
+
batch_size: 1000, batch_column_name: primary_key(table_name), progress: false, pause_ms: 50)
|
70
|
+
__ensure_not_in_transaction!
|
71
|
+
|
72
|
+
if !columns_and_values.is_a?(Array) || !columns_and_values.all? { |e| e.is_a?(Array) }
|
73
|
+
raise ArgumentError, "columns_and_values must be an array of arrays"
|
74
|
+
end
|
75
|
+
|
76
|
+
if progress
|
77
|
+
if progress == true
|
78
|
+
progress = ->(_) { print(".") }
|
79
|
+
elsif !progress.respond_to?(:call)
|
80
|
+
raise ArgumentError, "The progress body needs to be a callable."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
model = Utils.define_model(self, table_name)
|
85
|
+
|
86
|
+
conditions = columns_and_values.map do |(column_name, value)|
|
87
|
+
arel_column = model.arel_table[column_name]
|
88
|
+
arel_column.not_eq(value).or(arel_column.eq(nil))
|
89
|
+
end
|
90
|
+
|
91
|
+
batch_relation = model.where(conditions.inject(:and))
|
92
|
+
batch_relation = yield batch_relation if block_given?
|
93
|
+
|
94
|
+
iterator = BatchIterator.new(batch_relation)
|
95
|
+
iterator.each_batch(of: batch_size, column: batch_column_name) do |relation|
|
96
|
+
updates =
|
97
|
+
if Utils.ar_version <= 5.2
|
98
|
+
columns_and_values.map do |(column_name, value)|
|
99
|
+
# ActiveRecord <= 5.2 can't quote these - we need to handle these cases manually
|
100
|
+
case value
|
101
|
+
when Arel::Attributes::Attribute
|
102
|
+
"#{quote_column_name(column_name)} = #{quote_column_name(value.name)}"
|
103
|
+
when Arel::Nodes::SqlLiteral
|
104
|
+
"#{quote_column_name(column_name)} = #{value}"
|
105
|
+
when Arel::Nodes::NamedFunction
|
106
|
+
"#{quote_column_name(column_name)} = #{value.name}(#{quote_column_name(value.expressions.first.name)})"
|
107
|
+
else
|
108
|
+
"#{quote_column_name(column_name)} = #{quote(value)}"
|
109
|
+
end
|
110
|
+
end.join(", ")
|
111
|
+
else
|
112
|
+
columns_and_values.to_h
|
113
|
+
end
|
114
|
+
|
115
|
+
relation.update_all(updates)
|
116
|
+
|
117
|
+
progress.call(relation) if progress
|
118
|
+
|
119
|
+
sleep(pause_ms * 0.001) if pause_ms > 0
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Renames a column without requiring downtime
|
124
|
+
#
|
125
|
+
# The technique is built on top of database views, using the following steps:
|
126
|
+
# 1. Rename the table to some temporary name
|
127
|
+
# 2. Create a VIEW using the old table name with addition of a new column as an alias of the old one
|
128
|
+
# 3. Add a workaround for ActiveRecord's schema cache
|
129
|
+
#
|
130
|
+
# For example, to rename `name` column to `first_name` of the `users` table, we can run:
|
131
|
+
#
|
132
|
+
# BEGIN;
|
133
|
+
# ALTER TABLE users RENAME TO users_column_rename;
|
134
|
+
# CREATE VIEW users AS SELECT *, first_name AS name FROM users;
|
135
|
+
# COMMIT;
|
136
|
+
#
|
137
|
+
# As database views do not expose the underlying table schema (default values, not null constraints,
|
138
|
+
# indexes, etc), further steps are needed to update the application to use the new table name.
|
139
|
+
# ActiveRecord heavily relies on this data, for example, to initialize new models.
|
140
|
+
#
|
141
|
+
# To work around this limitation, we need to tell ActiveRecord to acquire this information
|
142
|
+
# from original table using the new table name (see notes).
|
143
|
+
#
|
144
|
+
# @param table_name [String, Symbol] table name
|
145
|
+
# @param column_name [String, Symbol] the name of the column to be renamed
|
146
|
+
# @param new_column_name [String, Symbol] new new name of the column
|
147
|
+
#
|
148
|
+
# @return [void]
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# initialize_column_rename(:users, :name, :first_name)
|
152
|
+
#
|
153
|
+
# @note
|
154
|
+
# Prior to using this method, you need to register the database table so that
|
155
|
+
# it instructs ActiveRecord to fetch the database table information (for SchemaCache)
|
156
|
+
# using the original table name (if it's present). Otherwise, fall back to the old table name:
|
157
|
+
#
|
158
|
+
# ```OnlineMigrations.config.column_renames[table_name] = { old_column_name => new_column_name }```
|
159
|
+
#
|
160
|
+
# Deploy this change before proceeding with this helper.
|
161
|
+
# This is necessary to avoid errors during a zero-downtime deployment.
|
162
|
+
#
|
163
|
+
# @note None of the DDL operations involving original table name can be performed
|
164
|
+
# until `finalize_column_rename` is run
|
165
|
+
#
|
166
|
+
def initialize_column_rename(table_name, column_name, new_column_name)
|
167
|
+
tmp_table = "#{table_name}_column_rename"
|
168
|
+
|
169
|
+
transaction do
|
170
|
+
rename_table(table_name, tmp_table)
|
171
|
+
execute("CREATE VIEW #{table_name} AS SELECT *, #{column_name} AS #{new_column_name} FROM #{tmp_table}")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Reverts operations performed by initialize_column_rename
|
176
|
+
#
|
177
|
+
# @param table_name [String, Symbol] table name
|
178
|
+
# @param _column_name [String, Symbol] the name of the column to be renamed.
|
179
|
+
# Passing this argument will make this change reversible in migration
|
180
|
+
# @param _new_column_name [String, Symbol] new new name of the column.
|
181
|
+
# Passing this argument will make this change reversible in migration
|
182
|
+
#
|
183
|
+
# @return [void]
|
184
|
+
#
|
185
|
+
# @example
|
186
|
+
# revert_initialize_column_rename(:users, :name, :first_name)
|
187
|
+
#
|
188
|
+
def revert_initialize_column_rename(table_name, _column_name = nil, _new_column_name = nil)
|
189
|
+
transaction do
|
190
|
+
execute("DROP VIEW #{table_name}")
|
191
|
+
rename_table("#{table_name}_column_rename", table_name)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Finishes the process of column rename
|
196
|
+
#
|
197
|
+
# @param (see #initialize_column_rename)
|
198
|
+
# @return [void]
|
199
|
+
#
|
200
|
+
# @example
|
201
|
+
# finalize_column_rename(:users, :name, :first_name)
|
202
|
+
#
|
203
|
+
def finalize_column_rename(table_name, column_name, new_column_name)
|
204
|
+
transaction do
|
205
|
+
execute("DROP VIEW #{table_name}")
|
206
|
+
rename_table("#{table_name}_column_rename", table_name)
|
207
|
+
rename_column(table_name, column_name, new_column_name)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Reverts operations performed by finalize_column_rename
|
212
|
+
#
|
213
|
+
# @param (see #initialize_column_rename)
|
214
|
+
# @return [void]
|
215
|
+
#
|
216
|
+
# @example
|
217
|
+
# revert_finalize_column_rename(:users, :name, :first_name)
|
218
|
+
#
|
219
|
+
def revert_finalize_column_rename(table_name, column_name, new_column_name)
|
220
|
+
tmp_table = "#{table_name}_column_rename"
|
221
|
+
|
222
|
+
transaction do
|
223
|
+
rename_column(table_name, new_column_name, column_name)
|
224
|
+
rename_table(table_name, tmp_table)
|
225
|
+
execute("CREATE VIEW #{table_name} AS SELECT *, #{column_name} AS #{new_column_name} FROM #{tmp_table}")
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Renames a table without requiring downtime
|
230
|
+
#
|
231
|
+
# The technique is built on top of database views, using the following steps:
|
232
|
+
# 1. Rename the database table
|
233
|
+
# 2. Create a database view using the old table name by pointing to the new table name
|
234
|
+
# 3. Add a workaround for ActiveRecord's schema cache
|
235
|
+
#
|
236
|
+
# For example, to rename `clients` table name to `users`, we can run:
|
237
|
+
#
|
238
|
+
# BEGIN;
|
239
|
+
# ALTER TABLE clients RENAME TO users;
|
240
|
+
# CREATE VIEW clients AS SELECT * FROM users;
|
241
|
+
# COMMIT;
|
242
|
+
#
|
243
|
+
# As database views do not expose the underlying table schema (default values, not null constraints,
|
244
|
+
# indexes, etc), further steps are needed to update the application to use the new table name.
|
245
|
+
# ActiveRecord heavily relies on this data, for example, to initialize new models.
|
246
|
+
#
|
247
|
+
# To work around this limitation, we need to tell ActiveRecord to acquire this information
|
248
|
+
# from original table using the new table name (see notes).
|
249
|
+
#
|
250
|
+
# @param table_name [String, Symbol]
|
251
|
+
# @param new_name [String, Symbol] table's new name
|
252
|
+
#
|
253
|
+
# @return [void]
|
254
|
+
#
|
255
|
+
# @example
|
256
|
+
# initialize_table_rename(:clients, :users)
|
257
|
+
#
|
258
|
+
# @note
|
259
|
+
# Prior to using this method, you need to register the database table so that
|
260
|
+
# it instructs ActiveRecord to fetch the database table information (for SchemaCache)
|
261
|
+
# using the new table name (if it's present). Otherwise, fall back to the old table name:
|
262
|
+
#
|
263
|
+
# ```
|
264
|
+
# OnlineMigrations.config.table_renames[old_table_name] = new_table_name
|
265
|
+
# ```
|
266
|
+
#
|
267
|
+
# Deploy this change before proceeding with this helper.
|
268
|
+
# This is necessary to avoid errors during a zero-downtime deployment.
|
269
|
+
#
|
270
|
+
# @note None of the DDL operations involving original table name can be performed
|
271
|
+
# until `finalize_table_rename` is run
|
272
|
+
#
|
273
|
+
def initialize_table_rename(table_name, new_name)
|
274
|
+
transaction do
|
275
|
+
rename_table(table_name, new_name)
|
276
|
+
execute("CREATE VIEW #{table_name} AS SELECT * FROM #{new_name}")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Reverts operations performed by initialize_table_rename
|
281
|
+
#
|
282
|
+
# @param (see #initialize_table_rename)
|
283
|
+
# @return [void]
|
284
|
+
#
|
285
|
+
# @example
|
286
|
+
# revert_initialize_table_rename(:clients, :users)
|
287
|
+
#
|
288
|
+
def revert_initialize_table_rename(table_name, new_name)
|
289
|
+
transaction do
|
290
|
+
execute("DROP VIEW IF EXISTS #{table_name}")
|
291
|
+
rename_table(new_name, table_name)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Finishes the process of table rename
|
296
|
+
#
|
297
|
+
# @param table_name [String, Symbol]
|
298
|
+
# @param _new_name [String, Symbol] table's new name. Passing this argument will make
|
299
|
+
# this change reversible in migration
|
300
|
+
# @return [void]
|
301
|
+
#
|
302
|
+
# @example
|
303
|
+
# finalize_table_rename(:users, :clients)
|
304
|
+
#
|
305
|
+
def finalize_table_rename(table_name, _new_name = nil)
|
306
|
+
execute("DROP VIEW IF EXISTS #{table_name}")
|
307
|
+
end
|
308
|
+
|
309
|
+
# Reverts operations performed by finalize_table_rename
|
310
|
+
#
|
311
|
+
# @param table_name [String, Symbol]
|
312
|
+
# @param new_name [String, Symbol] table's new name
|
313
|
+
# @return [void]
|
314
|
+
#
|
315
|
+
# @example
|
316
|
+
# revert_finalize_table_rename(:users, :clients)
|
317
|
+
#
|
318
|
+
def revert_finalize_table_rename(table_name, new_name)
|
319
|
+
execute("CREATE VIEW #{table_name} AS SELECT * FROM #{new_name}")
|
320
|
+
end
|
321
|
+
|
322
|
+
# Swaps two column names in a table
|
323
|
+
#
|
324
|
+
# This method is mostly intended for use as one of the steps for
|
325
|
+
# concurrent column type change
|
326
|
+
#
|
327
|
+
# @param table_name [String, Symbol]
|
328
|
+
# @param column1 [String, Symbol]
|
329
|
+
# @param column2 [String, Symbol]
|
330
|
+
# @return [void]
|
331
|
+
#
|
332
|
+
# @example
|
333
|
+
# swap_column_names(:files, :size_for_type_change, :size)
|
334
|
+
#
|
335
|
+
def swap_column_names(table_name, column1, column2)
|
336
|
+
transaction do
|
337
|
+
rename_column(table_name, column1, "#{column1}_tmp")
|
338
|
+
rename_column(table_name, column2, column1)
|
339
|
+
rename_column(table_name, "#{column1}_tmp", column2)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Adds a column with a default value without durable locks of the entire table
|
344
|
+
#
|
345
|
+
# This method runs the following steps:
|
346
|
+
#
|
347
|
+
# 1. Add the column allowing NULLs
|
348
|
+
# 2. Change the default value of the column to the specified value
|
349
|
+
# 3. Backfill all existing rows in batches
|
350
|
+
# 4. Set a `NOT NULL` constraint on the column if desired (the default).
|
351
|
+
#
|
352
|
+
# These steps ensure a column can be added to a large and commonly used table
|
353
|
+
# without locking the entire table for the duration of the table modification.
|
354
|
+
#
|
355
|
+
# For extra large tables (100s of millions of records) you may consider implementing
|
356
|
+
# the steps from this helper method yourself as a separate migrations, replacing step #3
|
357
|
+
# with the help of background migrations (see `backfill_column_in_background`).
|
358
|
+
#
|
359
|
+
# @param table_name [String, Symbol]
|
360
|
+
# @param column_name [String, Symbol]
|
361
|
+
# @param type [Symbol] type of new column
|
362
|
+
#
|
363
|
+
# @param options [Hash] `:batch_size`, `:batch_column_name`, `:progress`, and `:pause_ms`
|
364
|
+
# are directly passed to `update_column_in_batches` to control the backfilling process.
|
365
|
+
# Additional options (like `:limit`, etc) are forwarded to `add_column`
|
366
|
+
# @option options :default The column's default value
|
367
|
+
# @option options [Boolean] :null (true) Allows or disallows NULL values in the column
|
368
|
+
#
|
369
|
+
# @return [void]
|
370
|
+
#
|
371
|
+
# @example
|
372
|
+
# add_column_with_default(:users, :admin, :boolean, default: false, null: false)
|
373
|
+
#
|
374
|
+
# @example Additional column options
|
375
|
+
# add_column_with_default(:users, :twitter, :string, default: "", limit: 64)
|
376
|
+
#
|
377
|
+
# @example Additional batching options
|
378
|
+
# add_column_with_default(:users, :admin, :boolean, default: false,
|
379
|
+
# batch_size: 10_000, pause_ms: 100)
|
380
|
+
#
|
381
|
+
# @note This method should not be run within a transaction
|
382
|
+
# @note For PostgreSQL 11+ you can use `add_column` instead
|
383
|
+
#
|
384
|
+
def add_column_with_default(table_name, column_name, type, **options)
|
385
|
+
default = options.fetch(:default)
|
386
|
+
if default.is_a?(Proc) &&
|
387
|
+
ActiveRecord.version < Gem::Version.new("5.0.0.beta2") # https://github.com/rails/rails/pull/20005
|
388
|
+
raise ArgumentError, "Expressions as default are not supported"
|
389
|
+
end
|
390
|
+
|
391
|
+
if @connection.server_version >= 11_00_00 && !Utils.volatile_default?(self, type, default)
|
392
|
+
add_column(table_name, column_name, type, **options)
|
393
|
+
else
|
394
|
+
__ensure_not_in_transaction!
|
395
|
+
|
396
|
+
batch_options = options.extract!(:batch_size, :batch_column_name, :progress, :pause_ms)
|
397
|
+
|
398
|
+
if column_exists?(table_name, column_name)
|
399
|
+
Utils.say("Column was not created because it already exists (this may be due to an aborted migration "\
|
400
|
+
"or similar) table_name: #{table_name}, column_name: #{column_name}")
|
401
|
+
else
|
402
|
+
transaction do
|
403
|
+
add_column(table_name, column_name, type, **options.merge(default: nil, null: true))
|
404
|
+
change_column_default(table_name, column_name, default)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
update_column_in_batches(table_name, column_name, default, **batch_options)
|
409
|
+
|
410
|
+
allow_null = options.delete(:null) != false
|
411
|
+
if !allow_null
|
412
|
+
# A `NOT NULL` constraint for the column is functionally equivalent
|
413
|
+
# to creating a CHECK constraint `CHECK (column IS NOT NULL)` for the table
|
414
|
+
add_not_null_constraint(table_name, column_name, validate: false)
|
415
|
+
validate_not_null_constraint(table_name, column_name)
|
416
|
+
|
417
|
+
if @connection.server_version >= 12_00_00
|
418
|
+
# In PostgreSQL 12+ it is safe to "promote" a CHECK constraint to `NOT NULL` for the column
|
419
|
+
change_column_null(table_name, column_name, false)
|
420
|
+
remove_not_null_constraint(table_name, column_name)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Adds a NOT NULL constraint to the column
|
427
|
+
#
|
428
|
+
# @param table_name [String, Symbol]
|
429
|
+
# @param column_name [String, Symbol]
|
430
|
+
# @param name [String, Symbol] the constraint name.
|
431
|
+
# Defaults to `chk_rails_<identifier>`
|
432
|
+
# @param validate [Boolean] whether or not the constraint should be validated
|
433
|
+
#
|
434
|
+
# @return [void]
|
435
|
+
#
|
436
|
+
# @example
|
437
|
+
# add_not_null_constraint(:users, :email, validate: false)
|
438
|
+
#
|
439
|
+
def add_not_null_constraint(table_name, column_name, name: nil, validate: true)
|
440
|
+
if __column_not_nullable?(table_name, column_name) ||
|
441
|
+
__not_null_constraint_exists?(table_name, column_name, name: name)
|
442
|
+
Utils.say("NOT NULL constraint was not created: column #{table_name}.#{column_name} is already defined as `NOT NULL`")
|
443
|
+
else
|
444
|
+
expression = "#{column_name} IS NOT NULL"
|
445
|
+
name ||= __not_null_constraint_name(table_name, column_name)
|
446
|
+
add_check_constraint(table_name, expression, name: name, validate: false)
|
447
|
+
|
448
|
+
if validate
|
449
|
+
validate_not_null_constraint(table_name, column_name, name: name)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Validates a NOT NULL constraint on the column
|
455
|
+
#
|
456
|
+
# @param table_name [String, Symbol]
|
457
|
+
# @param column_name [String, Symbol]
|
458
|
+
# @param name [String, Symbol] the constraint name.
|
459
|
+
# Defaults to `chk_rails_<identifier>`
|
460
|
+
#
|
461
|
+
# @return [void]
|
462
|
+
#
|
463
|
+
# @example
|
464
|
+
# validate_not_null_constraint(:users, :email)
|
465
|
+
#
|
466
|
+
# @example Explicit name
|
467
|
+
# validate_not_null_constraint(:users, :email, name: "check_users_email_null")
|
468
|
+
#
|
469
|
+
def validate_not_null_constraint(table_name, column_name, name: nil)
|
470
|
+
name ||= __not_null_constraint_name(table_name, column_name)
|
471
|
+
validate_check_constraint(table_name, name: name)
|
472
|
+
end
|
473
|
+
|
474
|
+
# Removes a NOT NULL constraint from the column
|
475
|
+
#
|
476
|
+
# @param table_name [String, Symbol]
|
477
|
+
# @param column_name [String, Symbol]
|
478
|
+
# @param name [String, Symbol] the constraint name.
|
479
|
+
# Defaults to `chk_rails_<identifier>`
|
480
|
+
#
|
481
|
+
# @return [void]
|
482
|
+
#
|
483
|
+
# @example
|
484
|
+
# remove_not_null_constraint(:users, :email)
|
485
|
+
#
|
486
|
+
# @example Explicit name
|
487
|
+
# remove_not_null_constraint(:users, :email, name: "check_users_email_null")
|
488
|
+
#
|
489
|
+
def remove_not_null_constraint(table_name, column_name, name: nil)
|
490
|
+
name ||= __not_null_constraint_name(table_name, column_name)
|
491
|
+
remove_check_constraint(table_name, name: name)
|
492
|
+
end
|
493
|
+
|
494
|
+
# Adds a limit constraint to the text column
|
495
|
+
#
|
496
|
+
# @param table_name [String, Symbol]
|
497
|
+
# @param column_name [String, Symbol]
|
498
|
+
# @param name [String, Symbol] the constraint name.
|
499
|
+
# Defaults to `chk_rails_<identifier>`
|
500
|
+
# @param validate [Boolean] whether or not the constraint should be validated
|
501
|
+
#
|
502
|
+
# @return [void]
|
503
|
+
#
|
504
|
+
# @example
|
505
|
+
# add_text_limit_constraint(:users, :bio, 255)
|
506
|
+
#
|
507
|
+
# @note This helper must be used only with text columns
|
508
|
+
#
|
509
|
+
def add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true)
|
510
|
+
column = __column_for(table_name, column_name)
|
511
|
+
if column.type != :text
|
512
|
+
raise "add_text_limit_constraint must be used only with :text columns"
|
513
|
+
end
|
514
|
+
|
515
|
+
name ||= __text_limit_constraint_name(table_name, column_name)
|
516
|
+
|
517
|
+
if __text_limit_constraint_exists?(table_name, column_name, name: name)
|
518
|
+
Utils.say("Text limit constraint was not created: #{table_name}.#{column_name} is already has a limit")
|
519
|
+
else
|
520
|
+
add_check_constraint(
|
521
|
+
table_name,
|
522
|
+
"char_length(#{column_name}) <= #{limit}",
|
523
|
+
name: name,
|
524
|
+
validate: false
|
525
|
+
)
|
526
|
+
|
527
|
+
if validate
|
528
|
+
validate_text_limit_constraint(table_name, column_name, name: name)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Validates a limit constraint on the text column
|
534
|
+
#
|
535
|
+
# @param table_name [String, Symbol]
|
536
|
+
# @param column_name [String, Symbol]
|
537
|
+
# @param name [String, Symbol] the constraint name.
|
538
|
+
# Defaults to `chk_rails_<identifier>`
|
539
|
+
#
|
540
|
+
# @return [void]
|
541
|
+
#
|
542
|
+
# @example
|
543
|
+
# validate_text_limit_constraint(:users, :bio)
|
544
|
+
#
|
545
|
+
# @example Explicit name
|
546
|
+
# validate_text_limit_constraint(:users, :bio, name: "check_users_bio_max_length")
|
547
|
+
#
|
548
|
+
def validate_text_limit_constraint(table_name, column_name, name: nil)
|
549
|
+
name ||= __text_limit_constraint_name(table_name, column_name)
|
550
|
+
validate_check_constraint(table_name, name: name)
|
551
|
+
end
|
552
|
+
|
553
|
+
# Removes a limit constraint from the text column
|
554
|
+
#
|
555
|
+
# @param table_name [String, Symbol]
|
556
|
+
# @param column_name [String, Symbol]
|
557
|
+
# @param name [String, Symbol] the constraint name.
|
558
|
+
# Defaults to `chk_rails_<identifier>`
|
559
|
+
#
|
560
|
+
# @return [void]
|
561
|
+
#
|
562
|
+
# @example
|
563
|
+
# remove_text_limit_constraint(:users, :bio)
|
564
|
+
#
|
565
|
+
# @example Explicit name
|
566
|
+
# remove_not_null_constraint(:users, :bio, name: "check_users_bio_max_length")
|
567
|
+
#
|
568
|
+
def remove_text_limit_constraint(table_name, column_name, _limit = nil, name: nil)
|
569
|
+
name ||= __text_limit_constraint_name(table_name, column_name)
|
570
|
+
remove_check_constraint(table_name, name: name)
|
571
|
+
end
|
572
|
+
|
573
|
+
# Adds a reference to the table with minimal locking
|
574
|
+
#
|
575
|
+
# ActiveRecord adds an index non-`CONCURRENTLY` to references by default, which blocks writes.
|
576
|
+
# It also adds a validated foreign key by default, which blocks writes on both tables while
|
577
|
+
# validating existing rows.
|
578
|
+
#
|
579
|
+
# This method makes sure that an index is added `CONCURRENTLY` and the foreign key creation is performed
|
580
|
+
# in 2 steps: addition of invalid foreign key and a separate validation.
|
581
|
+
#
|
582
|
+
# @param table_name [String, Symbol] table name
|
583
|
+
# @param ref_name [String, Symbol] new column name
|
584
|
+
# @param options [Hash] look at
|
585
|
+
# https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference for available options
|
586
|
+
#
|
587
|
+
# @return [void]
|
588
|
+
#
|
589
|
+
# @example
|
590
|
+
# add_reference_concurrently(:projects, :user)
|
591
|
+
#
|
592
|
+
# @note This method should not be run within a transaction
|
593
|
+
#
|
594
|
+
def add_reference_concurrently(table_name, ref_name, **options)
|
595
|
+
__ensure_not_in_transaction!
|
596
|
+
|
597
|
+
column_name = "#{ref_name}_id"
|
598
|
+
unless column_exists?(table_name, column_name)
|
599
|
+
type = options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)
|
600
|
+
allow_null = options.fetch(:null, true)
|
601
|
+
add_column(table_name, column_name, type, null: allow_null)
|
602
|
+
end
|
603
|
+
|
604
|
+
# Always added by default in 5.0+
|
605
|
+
index = options.fetch(:index) { Utils.ar_version >= 5.0 }
|
606
|
+
|
607
|
+
if index
|
608
|
+
index = {} if index == true
|
609
|
+
index_columns = [column_name]
|
610
|
+
if options[:polymorphic]
|
611
|
+
index[:name] ||= "index_#{table_name}_on_#{ref_name}"
|
612
|
+
index_columns.unshift("#{ref_name}_type")
|
613
|
+
end
|
614
|
+
|
615
|
+
add_index(table_name, index_columns, **index.merge(algorithm: :concurrently))
|
616
|
+
end
|
617
|
+
|
618
|
+
foreign_key = options[:foreign_key]
|
619
|
+
|
620
|
+
if foreign_key
|
621
|
+
foreign_key = {} if foreign_key == true
|
622
|
+
|
623
|
+
foreign_table_name = Utils.foreign_table_name(ref_name, foreign_key)
|
624
|
+
add_foreign_key(table_name, foreign_table_name, **foreign_key.merge(validate: false))
|
625
|
+
|
626
|
+
if foreign_key[:validate] != false
|
627
|
+
validate_foreign_key(table_name, foreign_table_name, **foreign_key)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Extends default method to be idempotent and automatically recreate invalid indexes.
|
633
|
+
#
|
634
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index
|
635
|
+
#
|
636
|
+
def add_index(table_name, column_name, options = {})
|
637
|
+
algorithm = options[:algorithm]
|
638
|
+
|
639
|
+
__ensure_not_in_transaction! if algorithm == :concurrently
|
640
|
+
|
641
|
+
column_names = __index_column_names(column_name || options[:column])
|
642
|
+
|
643
|
+
index_name = options[:name]
|
644
|
+
index_name ||= index_name(table_name, column_names)
|
645
|
+
|
646
|
+
if index_exists?(table_name, column_name, **options)
|
647
|
+
schema = __schema_for_table(table_name)
|
648
|
+
|
649
|
+
if __index_valid?(index_name, schema: schema)
|
650
|
+
Utils.say("Index was not created because it already exists (this may be due to an aborted migration "\
|
651
|
+
"or similar): table_name: #{table_name}, column_name: #{column_name}")
|
652
|
+
return
|
653
|
+
else
|
654
|
+
Utils.say("Recreating invalid index: table_name: #{table_name}, column_name: #{column_name}")
|
655
|
+
remove_index(table_name, column_name, name: index_name, algorithm: algorithm)
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
disable_statement_timeout do
|
660
|
+
# "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
661
|
+
# It only conflicts with constraint validations, creating/removing indexes,
|
662
|
+
# and some other "ALTER TABLE"s.
|
663
|
+
super(table_name, column_name, **options.merge(name: index_name))
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
# Extends default method to be idempotent and accept `:algorithm` option for ActiveRecord <= 4.2.
|
668
|
+
#
|
669
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_index
|
670
|
+
#
|
671
|
+
def remove_index(table_name, column_name = nil, **options)
|
672
|
+
algorithm = options[:algorithm]
|
673
|
+
|
674
|
+
__ensure_not_in_transaction! if algorithm == :concurrently
|
675
|
+
|
676
|
+
column_names = __index_column_names(column_name || options[:column])
|
677
|
+
index_name = options[:name]
|
678
|
+
index_name ||= index_name(table_name, column_names)
|
679
|
+
|
680
|
+
if index_exists?(table_name, column_name, **options)
|
681
|
+
disable_statement_timeout do
|
682
|
+
# "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
683
|
+
# It only conflicts with constraint validations, other creating/removing indexes,
|
684
|
+
# and some "ALTER TABLE"s.
|
685
|
+
|
686
|
+
# ActiveRecord <= 4.2 does not support removing indexes concurrently
|
687
|
+
if Utils.ar_version <= 4.2 && algorithm == :concurrently
|
688
|
+
execute("DROP INDEX CONCURRENTLY #{quote_table_name(index_name)}")
|
689
|
+
else
|
690
|
+
super(table_name, **options.merge(column: column_names))
|
691
|
+
end
|
692
|
+
end
|
693
|
+
else
|
694
|
+
Utils.say("Index was not removed because it does not exist (this may be due to an aborted migration "\
|
695
|
+
"or similar): table_name: #{table_name}, column_name: #{column_name}")
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# Extends default method to be idempotent and accept `:validate` option for ActiveRecord < 5.2.
|
700
|
+
#
|
701
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
|
702
|
+
#
|
703
|
+
def add_foreign_key(from_table, to_table, validate: true, **options)
|
704
|
+
if foreign_key_exists?(from_table, **options.merge(to_table: to_table))
|
705
|
+
message = +"Foreign key was not created because it already exists " \
|
706
|
+
"(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}"
|
707
|
+
message << ", #{options.inspect}" if options.any?
|
708
|
+
|
709
|
+
Utils.say(message)
|
710
|
+
else
|
711
|
+
# ActiveRecord >= 5.2 supports adding non-validated foreign keys natively
|
712
|
+
options = options.dup
|
713
|
+
options[:column] ||= "#{to_table.to_s.singularize}_id"
|
714
|
+
options[:primary_key] ||= "id"
|
715
|
+
options[:name] ||= __foreign_key_name(to_table, options[:column])
|
716
|
+
|
717
|
+
query = +<<~SQL
|
718
|
+
ALTER TABLE #{from_table}
|
719
|
+
ADD CONSTRAINT #{options[:name]}
|
720
|
+
FOREIGN KEY (#{options[:column]})
|
721
|
+
REFERENCES #{to_table} (#{options[:primary_key]})
|
722
|
+
SQL
|
723
|
+
query << "#{__action_sql('DELETE', options[:on_delete])}\n" if options[:on_delete].present?
|
724
|
+
query << "#{__action_sql('UPDATE', options[:on_update])}\n" if options[:on_update].present?
|
725
|
+
query << "NOT VALID\n" if !validate
|
726
|
+
|
727
|
+
execute(query.squish)
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
# Extends default method with disabled statement timeout while validation is run
|
732
|
+
#
|
733
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_foreign_key
|
734
|
+
# @note This method was added in ActiveRecord 5.2
|
735
|
+
#
|
736
|
+
def validate_foreign_key(from_table, to_table = nil, **options)
|
737
|
+
fk_name_to_validate = __foreign_key_for!(from_table, to_table: to_table, **options).name
|
738
|
+
|
739
|
+
# Skip costly operation if already validated.
|
740
|
+
return if __constraint_validated?(from_table, fk_name_to_validate, type: :foreign_key)
|
741
|
+
|
742
|
+
disable_statement_timeout do
|
743
|
+
# "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
744
|
+
# It only conflicts with other validations, creating/removing indexes,
|
745
|
+
# and some other "ALTER TABLE"s.
|
746
|
+
execute("ALTER TABLE #{from_table} VALIDATE CONSTRAINT #{fk_name_to_validate}")
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
def foreign_key_exists?(from_table, to_table = nil, **options)
|
751
|
+
foreign_keys(from_table).any? { |fk| fk.defined_for?(to_table: to_table, **options) }
|
752
|
+
end
|
753
|
+
|
754
|
+
# Extends default method to be idempotent
|
755
|
+
#
|
756
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
|
757
|
+
# @note This method was added in ActiveRecord 6.1
|
758
|
+
#
|
759
|
+
def add_check_constraint(table_name, expression, validate: true, **options)
|
760
|
+
constraint_name = __check_constraint_name(table_name, expression: expression, **options)
|
761
|
+
|
762
|
+
if __check_constraint_exists?(table_name, constraint_name)
|
763
|
+
Utils.say("Check constraint was not created because it already exists (this may be due to an aborted migration "\
|
764
|
+
"or similar) table_name: #{table_name}, expression: #{expression}, constraint name: #{constraint_name}")
|
765
|
+
else
|
766
|
+
query = "ALTER TABLE #{table_name} ADD CONSTRAINT #{constraint_name} CHECK (#{expression})"
|
767
|
+
query += " NOT VALID" if !validate
|
768
|
+
|
769
|
+
execute(query)
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
# Extends default method with disabled statement timeout while validation is run
|
774
|
+
#
|
775
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_check_constraint
|
776
|
+
# @note This method was added in ActiveRecord 6.1
|
777
|
+
#
|
778
|
+
def validate_check_constraint(table_name, **options)
|
779
|
+
constraint_name = __check_constraint_name!(table_name, **options)
|
780
|
+
|
781
|
+
# Skip costly operation if already validated.
|
782
|
+
return if __constraint_validated?(table_name, constraint_name, type: :check)
|
783
|
+
|
784
|
+
disable_statement_timeout do
|
785
|
+
# "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
786
|
+
# It only conflicts with other validations, creating/removing indexes,
|
787
|
+
# and some other "ALTER TABLE"s.
|
788
|
+
execute("ALTER TABLE #{table_name} VALIDATE CONSTRAINT #{constraint_name}")
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
if Utils.ar_version < 6.1
|
793
|
+
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
|
794
|
+
# @note This method was added in ActiveRecord 6.1
|
795
|
+
#
|
796
|
+
def remove_check_constraint(table_name, expression = nil, **options)
|
797
|
+
constraint_name = __check_constraint_name!(table_name, expression: expression, **options)
|
798
|
+
execute("ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint_name}")
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
if Utils.ar_version <= 4.2
|
803
|
+
# @private
|
804
|
+
def views
|
805
|
+
select_values(<<-SQL, "SCHEMA")
|
806
|
+
SELECT c.relname
|
807
|
+
FROM pg_class c
|
808
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
809
|
+
WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
|
810
|
+
AND n.nspname = ANY (current_schemas(false))
|
811
|
+
SQL
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
# Disables statement timeout while executing &block
|
816
|
+
#
|
817
|
+
# Long-running migrations may take more than the timeout allowed by the database.
|
818
|
+
# Disable the session's statement timeout to ensure migrations don't get killed prematurely.
|
819
|
+
#
|
820
|
+
# Statement timeouts are already disabled in `add_index`, `remove_index`,
|
821
|
+
# `validate_foreign_key`, and `validate_check_constraint` helpers.
|
822
|
+
#
|
823
|
+
# @return [void]
|
824
|
+
#
|
825
|
+
# @example
|
826
|
+
# disable_statement_timeout do
|
827
|
+
# add_index(:users, :email, unique: true, algorithm: :concurrently)
|
828
|
+
# end
|
829
|
+
#
|
830
|
+
def disable_statement_timeout
|
831
|
+
prev_value = select_value("SHOW statement_timeout")
|
832
|
+
execute("SET statement_timeout TO 0")
|
833
|
+
|
834
|
+
yield
|
835
|
+
ensure
|
836
|
+
execute("SET statement_timeout TO #{quote(prev_value)}")
|
837
|
+
end
|
838
|
+
|
839
|
+
# @private
|
840
|
+
# Executes the block with a retry mechanism that alters the `lock_timeout`
|
841
|
+
# and sleep time between attempts.
|
842
|
+
#
|
843
|
+
def with_lock_retries(&block)
|
844
|
+
__ensure_not_in_transaction!
|
845
|
+
|
846
|
+
retrier = OnlineMigrations.config.lock_retrier
|
847
|
+
retrier.connection = self
|
848
|
+
retrier.with_lock_retries(&block)
|
849
|
+
end
|
850
|
+
|
851
|
+
private
|
852
|
+
# Private methods are prefixed with `__` to avoid clashes with existing or future
|
853
|
+
# ActiveRecord methods
|
854
|
+
def __ensure_not_in_transaction!(method_name = caller[0])
|
855
|
+
if transaction_open?
|
856
|
+
raise <<~MSG
|
857
|
+
`#{method_name}` cannot run inside a transaction block.
|
858
|
+
|
859
|
+
You can remove transaction block by calling `disable_ddl_transaction!` in the body of
|
860
|
+
your migration class.
|
861
|
+
MSG
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
def __column_not_nullable?(table_name, column_name)
|
866
|
+
schema = __schema_for_table(table_name)
|
867
|
+
|
868
|
+
query = <<~SQL
|
869
|
+
SELECT is_nullable
|
870
|
+
FROM information_schema.columns
|
871
|
+
WHERE table_schema = #{schema}
|
872
|
+
AND table_name = #{quote(table_name)}
|
873
|
+
AND column_name = #{quote(column_name)}
|
874
|
+
SQL
|
875
|
+
|
876
|
+
select_value(query) == "NO"
|
877
|
+
end
|
878
|
+
|
879
|
+
def __not_null_constraint_exists?(table_name, column_name, name: nil)
|
880
|
+
name ||= __not_null_constraint_name(table_name, column_name)
|
881
|
+
__check_constraint_exists?(table_name, name)
|
882
|
+
end
|
883
|
+
|
884
|
+
def __not_null_constraint_name(table_name, column_name)
|
885
|
+
__check_constraint_name(table_name, expression: "#{column_name}_not_null")
|
886
|
+
end
|
887
|
+
|
888
|
+
def __text_limit_constraint_name(table_name, column_name)
|
889
|
+
__check_constraint_name(table_name, expression: "#{column_name}_max_length")
|
890
|
+
end
|
891
|
+
|
892
|
+
def __text_limit_constraint_exists?(table_name, column_name, name: nil)
|
893
|
+
name ||= __text_limit_constraint_name(table_name, column_name)
|
894
|
+
__check_constraint_exists?(table_name, name)
|
895
|
+
end
|
896
|
+
|
897
|
+
def __index_column_names(column_names)
|
898
|
+
if column_names.is_a?(String) && /\W/.match?(column_names)
|
899
|
+
column_names
|
900
|
+
else
|
901
|
+
Array(column_names)
|
902
|
+
end
|
903
|
+
end
|
904
|
+
|
905
|
+
def __index_valid?(index_name, schema:)
|
906
|
+
# ActiveRecord <= 4.2 returns a string, instead of automatically casting to boolean
|
907
|
+
valid = select_value <<~SQL
|
908
|
+
SELECT indisvalid
|
909
|
+
FROM pg_index i
|
910
|
+
JOIN pg_class c
|
911
|
+
ON i.indexrelid = c.oid
|
912
|
+
JOIN pg_namespace n
|
913
|
+
ON c.relnamespace = n.oid
|
914
|
+
WHERE n.nspname = #{schema}
|
915
|
+
AND c.relname = #{quote(index_name)}
|
916
|
+
SQL
|
917
|
+
|
918
|
+
Utils.to_bool(valid)
|
919
|
+
end
|
920
|
+
|
921
|
+
def __column_for(table_name, column_name)
|
922
|
+
column_name = column_name.to_s
|
923
|
+
|
924
|
+
columns(table_name).find { |c| c.name == column_name } ||
|
925
|
+
raise("No such column: #{table_name}.#{column_name}")
|
926
|
+
end
|
927
|
+
|
928
|
+
def __action_sql(action, dependency)
|
929
|
+
case dependency
|
930
|
+
when :nullify then "ON #{action} SET NULL"
|
931
|
+
when :cascade then "ON #{action} CASCADE"
|
932
|
+
when :restrict then "ON #{action} RESTRICT"
|
933
|
+
else
|
934
|
+
raise ArgumentError, <<~MSG
|
935
|
+
'#{dependency}' is not supported for :on_update or :on_delete.
|
936
|
+
Supported values are: :nullify, :cascade, :restrict
|
937
|
+
MSG
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
def __copy_foreign_key(fk, to_column, **options)
|
942
|
+
fkey_options = {
|
943
|
+
column: to_column,
|
944
|
+
primary_key: options[:primary_key] || fk.primary_key,
|
945
|
+
on_delete: fk.on_delete,
|
946
|
+
on_update: fk.on_update,
|
947
|
+
validate: false,
|
948
|
+
}
|
949
|
+
fkey_options[:name] = options[:name] if options[:name]
|
950
|
+
|
951
|
+
add_foreign_key(
|
952
|
+
fk.from_table,
|
953
|
+
fk.to_table,
|
954
|
+
**fkey_options
|
955
|
+
)
|
956
|
+
|
957
|
+
if !fk.respond_to?(:validated?) || fk.validated?
|
958
|
+
validate_foreign_key(fk.from_table, fk.to_table, column: to_column, **options)
|
959
|
+
end
|
960
|
+
end
|
961
|
+
|
962
|
+
def __foreign_key_name(table_name, column_name)
|
963
|
+
identifier = "#{table_name}_#{column_name}_fk"
|
964
|
+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
965
|
+
|
966
|
+
"fk_rails_#{hashed_identifier}"
|
967
|
+
end
|
968
|
+
|
969
|
+
if Utils.ar_version <= 4.2
|
970
|
+
def foreign_key_for(from_table, **options)
|
971
|
+
foreign_keys(from_table).detect { |fk| fk.defined_for?(**options) }
|
972
|
+
end
|
973
|
+
end
|
974
|
+
|
975
|
+
def __foreign_key_for!(from_table, **options)
|
976
|
+
foreign_key_for(from_table, **options) ||
|
977
|
+
raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options[:to_table] || options}")
|
978
|
+
end
|
979
|
+
|
980
|
+
def __constraint_validated?(table_name, name, type:)
|
981
|
+
schema = __schema_for_table(table_name)
|
982
|
+
contype = type == :check ? "c" : "f"
|
983
|
+
|
984
|
+
validated = select_value(<<~SQL)
|
985
|
+
SELECT convalidated
|
986
|
+
FROM pg_catalog.pg_constraint con
|
987
|
+
INNER JOIN pg_catalog.pg_namespace nsp
|
988
|
+
ON nsp.oid = con.connamespace
|
989
|
+
WHERE con.conrelid = #{quote(table_name)}::regclass
|
990
|
+
AND con.conname = #{quote(name)}
|
991
|
+
AND con.contype = '#{contype}'
|
992
|
+
AND nsp.nspname = #{schema}
|
993
|
+
SQL
|
994
|
+
|
995
|
+
Utils.to_bool(validated)
|
996
|
+
end
|
997
|
+
|
998
|
+
def __check_constraint_name!(table_name, expression: nil, **options)
|
999
|
+
constraint_name = __check_constraint_name(table_name, expression: expression, **options)
|
1000
|
+
|
1001
|
+
if __check_constraint_exists?(table_name, constraint_name)
|
1002
|
+
constraint_name
|
1003
|
+
else
|
1004
|
+
raise(ArgumentError, "Table '#{table_name}' has no check constraint for #{expression || options}")
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def __check_constraint_name(table_name, **options)
|
1009
|
+
options.fetch(:name) do
|
1010
|
+
expression = options.fetch(:expression)
|
1011
|
+
identifier = "#{table_name}_#{expression}_chk"
|
1012
|
+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
1013
|
+
|
1014
|
+
"chk_rails_#{hashed_identifier}"
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def __check_constraint_exists?(table_name, constraint_name)
|
1019
|
+
schema = __schema_for_table(table_name)
|
1020
|
+
|
1021
|
+
check_sql = <<~SQL.squish
|
1022
|
+
SELECT COUNT(*)
|
1023
|
+
FROM pg_catalog.pg_constraint con
|
1024
|
+
INNER JOIN pg_catalog.pg_class cl
|
1025
|
+
ON cl.oid = con.conrelid
|
1026
|
+
INNER JOIN pg_catalog.pg_namespace nsp
|
1027
|
+
ON nsp.oid = con.connamespace
|
1028
|
+
WHERE con.contype = 'c'
|
1029
|
+
AND con.conname = #{quote(constraint_name)}
|
1030
|
+
AND cl.relname = #{quote(table_name)}
|
1031
|
+
AND nsp.nspname = #{schema}
|
1032
|
+
SQL
|
1033
|
+
|
1034
|
+
select_value(check_sql).to_i > 0
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
def __schema_for_table(table_name)
|
1038
|
+
_, schema = table_name.to_s.split(".").reverse
|
1039
|
+
schema ? quote(schema) : "current_schema()"
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
end
|