Skip to content

Instantly share code, notes, and snippets.

@voltechs
Created March 25, 2025 19:27
Show Gist options
  • Save voltechs/92b69fc5422ba5f9b27379d70eb12057 to your computer and use it in GitHub Desktop.
Save voltechs/92b69fc5422ba5f9b27379d70eb12057 to your computer and use it in GitHub Desktop.
Rails Migration from `bigint` to `uuid`
class AddUuidToAllTables < ActiveRecord::Migration[7.0]
TABLES = [
:users,
:posts,
# list tables
]
def up
TABLES.each do |table|
add_column table, :pk_uuid, :uuid, default: "gen_random_uuid()", null: false
add_index table, :pk_uuid, unique: true
end
end
def down
TABLES.each do |table|
remove_column table, :pk_uuid
end
end
end
class ChangePkInTablesWithBelongsToRelationsOnly < ActiveRecord::Migration[7.0]
TABLES = [
:users,
:posts,
# list tables
]
def up
TABLES.each do |table|
rename_column table, :id, :numeric_id
rename_column table, :pk_uuid, :id
change_pk(table)
end
end
def down
TABLES.each do |table|
rename_column table, :id, :pk_uuid
rename_column table, :numeric_id, :id
change_pk(table)
end
end
def change_pk(table)
execute "ALTER TABLE #{table} DROP CONSTRAINT #{table}_pkey CASCADE;"
execute "ALTER TABLE #{table} ADD PRIMARY KEY (id);"
end
end
class UpdateUuidFkValues < ActiveRecord::Migration[7.1]
TABLES_WITH_RELATIONS = {
posts: {
users: { user_id: :user_uuid },
},
}
def up
TABLES_WITH_RELATIONS.each do |parent_table, child_mappings|
child_mappings.each do |child_table, fk_map|
fk_map.each do |old_fk_sym, new_fk_sym|
# Build the update query to assign the proper UUID from the parent
execute <<-SQL.strip_heredoc
UPDATE #{child_table}
SET #{new_fk_sym}
= (
SELECT #{parent_table}.id
FROM #{parent_table}
WHERE #{parent_table}.numeric_id = #{child_table}.#{old_fk_sym}
)
SQL
end
end
end
end
def down
# no-op since data cannot be rolled back
end
end
class RenameUuidFkColumnsToOldName < ActiveRecord::Migration[7.1]
TABLES_WITH_RELATIONS = {
posts: {
users: { user_id: :user_uuid },
},
}
def up
TABLES_WITH_RELATIONS.each do |table, related_tables|
# Remove existing foreign keys on child tables safely.
remove_foreign_keys(related_tables)
# Migrate foreign keys for each child table referencing the given table.
related_tables.each do |child_table, mapping|
mapping.each do |old_fk_sym, new_fk_sym|
old_fk_name = old_fk_sym.to_s # e.g. "user_id"
uuid_fk_name = new_fk_sym.to_s # e.g. "user_uuid"
numeric_fk_name = "numeric_#{old_fk_name}".to_sym # e.g. :numeric_user_id
change_column_null child_table, old_fk_name, true
# Rename the existing FK column to a temporary numeric column if not done already.
unless column_exists?(child_table, numeric_fk_name)
rename_column child_table, old_fk_name, numeric_fk_name
end
# If the UUID column exists and the "old" column is not present, rename it.
if column_exists?(child_table, uuid_fk_name) && !column_exists?(child_table, old_fk_name)
rename_column child_table, uuid_fk_name, old_fk_name
end
add_foreign_key child_table, table, column: old_fk_name
# Now that the FK is linked, drop the temporary numeric column.
if column_exists?(child_table, numeric_fk_name)
remove_column child_table, numeric_fk_name
end
end
end
end
end
def down
TABLES_WITH_RELATIONS.each do |table, related_tables|
# Remove foreign keys from child tables safely.
remove_foreign_keys(related_tables)
# Rollback FK migration for each child table.
related_tables.each do |child_table, mapping|
mapping.each do |old_fk_sym, new_fk_sym|
old_fk_name = old_fk_sym.to_s
uuid_fk_name = new_fk_sym.to_s
numeric_fk_name = "numeric_#{old_fk_name}".to_sym
# Reverse the renaming.
if column_exists?(child_table, old_fk_name) && !column_exists?(child_table, uuid_fk_name)
rename_column child_table, old_fk_name, uuid_fk_name
end
# Since the numeric column was dropped in up, re-create it if needed.
unless column_exists?(child_table, numeric_fk_name)
add_column child_table, numeric_fk_name, :bigint
end
if column_exists?(child_table, numeric_fk_name) && !column_exists?(child_table, old_fk_name)
rename_column child_table, numeric_fk_name, old_fk_name
end
add_foreign_key child_table, table, column: old_fk_name
change_column_null child_table, old_fk_name, false
end
end
end
end
# Helper that checks if a foreign key exists before attempting its removal.
def remove_foreign_keys(related_tables)
related_tables.each do |child_table, mapping|
mapping.each do |old_fk_sym, _|
column_name = old_fk_sym.to_s
fks = ActiveRecord::Base.connection.foreign_keys(child_table)
if fks.map(&:column).include?(column_name)
remove_foreign_key child_table, column: column_name
end
end
end
end
def change_pk(table)
execute "ALTER TABLE #{table} DROP CONSTRAINT #{table}_pkey;"
execute "ALTER TABLE #{table} ADD PRIMARY KEY (id);"
end
end
#!/usr/bin/env ruby
require_relative '../config/environment'
require 'active_record'
require 'active_support/inflector'
# Ensure Rails is loaded appropriately so that ActiveRecord::Base.connection is available.
# For example, you might need to load config/environment if running stand-alone.
# require_relative '../config/environment'
def build_fk_mapping
mapping = {}
# Iterate over every table in the current connection.
ActiveRecord::Base.connection.tables.each do |child_table|
fks = ActiveRecord::Base.connection.foreign_keys(child_table)
fks.each do |fk|
# Use the actual referenced table from the FK definition.
referenced_table = fk.to_table.to_sym
mapping[referenced_table] ||= {}
mapping[referenced_table][child_table] ||= {}
old_fk = fk.column.to_sym
# Derive new FK column name. If the existing column ends in "_id", replace it with "_uuid".
new_fk = if fk.column.to_s.end_with?('_id')
fk.column.to_s.sub(/_id$/, '_uuid').to_sym
else
"#{fk.column}_uuid".to_sym
end
mapping[referenced_table][child_table][old_fk] = new_fk
end
end
mapping.reject { |_, child_mapping| child_mapping.empty? }
end
def generate_relations_output(mapping)
puts "TABLES_WITH_RELATIONS = {"
mapping.each do |referenced_table, child_mapping|
puts " #{referenced_table}: {"
child_mapping.each do |child_table, fk_map|
mapping_str = fk_map.map { |old_fk, new_fk| "#{old_fk}: :#{new_fk}" }.join(", ")
puts " #{child_table}: { #{mapping_str} },"
end
puts " },"
end
puts "}"
end
# -------------------------------------------------------------------
fk_mapping = build_fk_mapping
generate_relations_output(fk_mapping)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment