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

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.

diff --git a/script/db_timestamps_mover.rb b/script/db_timestamps_mover.rb
index 593506d..ad08569 100644
--- a/script/db_timestamps_mover.rb
+++ b/script/db_timestamps_mover.rb
@@ -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
 

GitHub sha: 1656b7ed0182c6b744d630e2f25315846a23dba5

This commit appears in #14027 which was approved by eviltrout. It was merged by AndrewPrigorshnev.