Last active
July 24, 2022 22:06
-
-
Save kuczmama/152d762177968f7192df1dea184e3370 to your computer and use it in GitHub Desktop.
Migrate a rails project to use uuids
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Inspired by http://www.madebyloren.com/posts/migrating-to-uuids-as-primary-keys | |
task id_to_uuid: :environment do | |
puts "[START] Convert id to uuid" | |
ActiveRecord::Base.connection.enable_extension 'uuid-ossp' unless ActiveRecord::Base.connection.extensions.include? 'uuid-ossp' | |
ActiveRecord::Base.connection.enable_extension 'pgcrypto' unless ActiveRecord::Base.connection.extensions.include? 'pgcrypto' | |
table_names = ActiveRecord::Base.connection.tables - ["schema_migrations", "ar_internal_metadata", "migration_validators"] | |
table_names.each do |table_name| | |
puts "[CREATE] uuid column for #{table_name}" | |
#Make sure the column is a uuid if not delete it and then create it | |
if ActiveRecord::Migration.column_exists? table_name, :uuid | |
column_type = ActiveRecord::Migration.columns(table_name).select{|c| c.name == "uuid"}.try(:first).try(:sql_type_metadata).try(:type) | |
if column_type && column_type != :uuid | |
ActiveRecord::Migration.remove_column(table_name, :uuid) | |
end | |
end | |
# Create it if it doesn't exist | |
if !ActiveRecord::Migration.column_exists? table_name, :uuid | |
ActiveRecord::Migration.add_column table_name, :uuid, :uuid, default: "uuid_generate_v4()", null: false | |
end | |
end | |
# The strategy here has three steps. | |
# For each association: | |
# 1) write the association's uuid to a temporary foreign key _uuid column, | |
# 2) For each association set the value of the _uuid column | |
# 3) remove the _id column and | |
# 4) rename the _uuid column to _id, effectively migrating our foreign keys to UUIDs while sticking with the _id convention. | |
table_names.each do |table_name| | |
puts "[UPDATE] change id to uuid #{table_name}" | |
model = table_name.singularize.camelize.constantize | |
id_columns = model.column_names.select{|c| c.end_with?("_id")} | |
# write the association's uuid to a temporary foreign key _uuid column | |
# eg. Message.room_id => Message.room_uuid | |
model.reflections.each do|k, v| | |
begin | |
association_id_col = v.foreign_key | |
# Error checking | |
# Make sure the relationship actually currently exists | |
next unless id_columns.include?(association_id_col) | |
# Check that there is at | |
# 1) Create temporary _uuid column set to nulll, | |
tmp_uuid_column_name = column_name_to_uuid(association_id_col) | |
unless ActiveRecord::Migration.column_exists?(table_name, tmp_uuid_column_name) | |
puts "[CREATE] #{table_name}.#{tmp_uuid_column_name}" | |
ActiveRecord::Migration.add_column(table_name, tmp_uuid_column_name, :uuid) | |
end | |
# 2) For each association set the value of the _uuid column | |
# | |
# For example. Assume the following example | |
# | |
# message.room_id = 1 | |
# room = Room.find(1) | |
# room.uuid = 0x123 | |
# message.room_uuid = 0x123 | |
# | |
association_klass = v.klass | |
model.unscoped.all.each do |inst| | |
next unless inst.present? | |
association = association_klass.find_by(id: inst.try(association_id_col.try(:to_sym))) | |
next unless association.present? | |
inst.update_column(tmp_uuid_column_name, association.try(:uuid)) | |
end | |
# 3) Remove id column | |
ActiveRecord::Migration.remove_column table_name, association_id_col if ActiveRecord::Migration.column_exists?(table_name, association_id_col) | |
# 4) Rename uuid_col_name to id | |
ActiveRecord::Migration.rename_column table_name, tmp_uuid_column_name, association_id_col | |
rescue => e | |
puts "Error: #{e} continuing" | |
next | |
end | |
end | |
# Make each temp _uuid column linked up | |
# eg. Message.find(1).room_uuid = Message.find(1).room.uuid | |
puts "[UPDATE] #{model}.uuid to association uuid" | |
end | |
## Migrate primary keys to uuids | |
table_names.each do |table_name| | |
if ActiveRecord::Migration.column_exists?(table_name, :id) && ActiveRecord::Migration.column_exists?(table_name, :uuid) | |
ActiveRecord::Base.connection.execute %Q{ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey CASCADE} rescue nil | |
ActiveRecord::Migration.remove_column(table_name, :id) | |
ActiveRecord::Migration.rename_column( table_name, :uuid, :id) if ActiveRecord::Migration.column_exists?(table_name, :uuid) | |
ActiveRecord::Base.connection.execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (id)" | |
ActiveRecord::Base.connection.execute %Q{DROP SEQUENCE IF EXISTS #{table_name}_id_seq CASCADE} rescue nil | |
end | |
end | |
end | |
# Add uuid to the id | |
# EG. column_name_to_uuid("room_id") => "room_uuid" | |
# EG. column_name_to_uuid("room_ids") => "room_uuids" | |
def column_name_to_uuid(column_name) | |
*a, b = column_name.split("_id", -1) | |
a.join("_id") + "_uuid" + b | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment