DEV: Make db_timestamp_mover work with tables with unique constraints (#14027)

Some tables in the database have constraints on columns with dates. Because of them, the script for moving timestamps can fail from time to time. This PR makes the script work with such tables.

In general, in PostgreSQL it is not always possible to defer constraint checks to the transaction commit (Primary Keys and Unique Constraints can be deferred, but them should be declared as DEFERRABLE to make it possible. Indices created with CREATE UNIQUE INDEX can't be deferred at all).

Since we can't defer constraint checks, I've made it work using a little hack. For example, if we need to move all timestamps by one day, the script will move timestamps by 1000 years and one day, and then return timestamps back by 1000 years. The script use this hack only for columns that have unique constraints.
This commit is contained in:
Andrei Prigorshnev 2021-08-12 19:24:21 +04:00 committed by GitHub
parent d27d7c8cca
commit 1656b7ed01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 50 additions and 3 deletions

View File

@ -34,7 +34,11 @@ class TimestampsUpdater
next if @ignore_tables.include? table
column = c["column_name"]
move_timestamps table, column, days
if has_unique_index(table, column)
move_timestamps_respect_constraints(table, column, days)
else
move_timestamps(table, column, days)
end
end
end
end
@ -60,11 +64,54 @@ class TimestampsUpdater
@raw_connection.exec(sql)
end
def has_unique_index(table, column)
# This detects unique indices created with "CREATE UNIQUE INDEX".
# This also detects unique constraints and primary keys,
# because postgresql creates unique indices for them.
sql = <<~SQL
SELECT 1
FROM pg_class t,
pg_class i,
pg_index ix,
pg_attribute a,
pg_namespace ns
WHERE t.oid = ix.indrelid
AND i.oid = ix.indexrelid
AND a.attrelid = t.oid
AND a.attnum = ANY (ix.indkey)
AND t.relnamespace = ns.oid
AND ns.nspname = '#{@schema}'
AND t.relname = '#{table}'
AND a.attname = '#{column}'
AND ix.indisunique
LIMIT 1;
SQL
result = @raw_connection.exec(sql)
result.any?
end
def move_timestamps(table_name, column_name, days)
operator = days < 0 ? "-" : "+"
interval_expression = "#{operator} INTERVAL '#{days.abs} days'"
update_table(table_name, column_name, interval_expression)
end
def move_timestamps_respect_constraints(table_name, column_name, days)
# add 1000 years to the interval to avoid uniqueness conflicts:
operator = days < 0 ? "-" : "+"
interval_expression = "#{operator} INTERVAL '1000 years #{days.abs} days'"
update_table(table_name, column_name, interval_expression)
# return back by 1000 years:
operator = days < 0 ? "+" : "-"
interval_expression = "#{operator} INTERVAL '1000 years'"
update_table(table_name, column_name, interval_expression)
end
def update_table(table_name, column_name, interval_expression)
sql = <<~SQL
UPDATE #{table_name}
SET #{column_name} = #{column_name} #{operator} INTERVAL '#{days.abs} day'
SET #{column_name} = #{column_name} #{interval_expression}
SQL
@raw_connection.exec(sql)
end
@ -79,7 +126,7 @@ def is_date?(string)
end
def create_updater
ignore_tables = %w[application_requests given_daily_likes user_second_factors user_visits]
ignore_tables = %w[user_second_factors]
TimestampsUpdater.new "public", ignore_tables
end