From b7404087bd877153e2101438ad3e8203cd1aa13d Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 17 Nov 2025 20:31:27 +0100 Subject: [PATCH] Fix schema incomatibilities with production --- ..._endpoint_to_text_in_push_subscriptions.rb | 14 +++ ...ternal_account_id_to_bigint_in_accounts.rb | 5 + db/schema.rb | 8 +- script/import-sqlite-database.rb | 101 ++++++++++++------ 4 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 db/migrate/20251117190817_change_endpoint_to_text_in_push_subscriptions.rb create mode 100644 db/migrate/20251117192434_change_external_account_id_to_bigint_in_accounts.rb diff --git a/db/migrate/20251117190817_change_endpoint_to_text_in_push_subscriptions.rb b/db/migrate/20251117190817_change_endpoint_to_text_in_push_subscriptions.rb new file mode 100644 index 000000000..636a91bb9 --- /dev/null +++ b/db/migrate/20251117190817_change_endpoint_to_text_in_push_subscriptions.rb @@ -0,0 +1,14 @@ +class ChangeEndpointToTextInPushSubscriptions < ActiveRecord::Migration[8.2] + def change + # Remove foreign key first, then the index + remove_foreign_key :push_subscriptions, :users + remove_index :push_subscriptions, column: [:user_id, :endpoint] + + # Change the column type + change_column :push_subscriptions, :endpoint, :text + + # Re-add the index and foreign key + add_index :push_subscriptions, [:user_id, :endpoint], unique: true, length: { endpoint: 255 } + add_foreign_key :push_subscriptions, :users + end +end diff --git a/db/migrate/20251117192434_change_external_account_id_to_bigint_in_accounts.rb b/db/migrate/20251117192434_change_external_account_id_to_bigint_in_accounts.rb new file mode 100644 index 000000000..d9671f8da --- /dev/null +++ b/db/migrate/20251117192434_change_external_account_id_to_bigint_in_accounts.rb @@ -0,0 +1,5 @@ +class ChangeExternalAccountIdToBigintInAccounts < ActiveRecord::Migration[8.2] + def change + change_column :accounts, :external_account_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index 62114dd17..ea70eb668 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_11_14_183203) do +ActiveRecord::Schema[8.2].define(version: 2025_11_17_192434) do create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "accessed_at" t.uuid "account_id", null: false @@ -38,7 +38,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_14_183203) do create_table "accounts", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "cards_count", default: 0, null: false t.datetime "created_at", null: false - t.integer "external_account_id" + t.bigint "external_account_id" t.string "name", null: false t.datetime "updated_at", null: false t.index ["external_account_id"], name: "index_accounts_on_external_account_id", unique: true @@ -375,13 +375,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_14_183203) do t.uuid "account_id", null: false t.string "auth_key" t.datetime "created_at", null: false - t.string "endpoint" + t.text "endpoint" t.string "p256dh_key" t.datetime "updated_at", null: false t.string "user_agent" t.uuid "user_id", null: false t.index ["account_id"], name: "index_push_subscriptions_on_account_id" - t.index ["user_id", "endpoint"], name: "index_push_subscriptions_on_user_id_and_endpoint", unique: true + t.index ["user_id", "endpoint"], name: "index_push_subscriptions_on_user_id_and_endpoint", unique: true, length: { endpoint: 255 } end create_table "reactions", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| diff --git a/script/import-sqlite-database.rb b/script/import-sqlite-database.rb index 719123198..86a16822a 100755 --- a/script/import-sqlite-database.rb +++ b/script/import-sqlite-database.rb @@ -4,6 +4,8 @@ require_relative "../config/environment" require "pathname" require "optparse" +class AccountExistsError < StandardError; end + class Import FIX_LINK_HOSTS = { "fizzy.37signals.com" => "app.fizzy.do", @@ -11,12 +13,13 @@ class Import "app.box-car.com" => "app.fizzy.do" }.freeze - attr_reader :db_path, :untenanted_db_path + attr_reader :db_path, :untenanted_db_path, :skip_already_imported attr_reader :account, :tenant, :mapping - def initialize(db_path, untenanted_db_path) + def initialize(db_path, untenanted_db_path, skip_already_imported: false) @db_path = Pathname(db_path) @untenanted_db_path = Pathname(untenanted_db_path) + @skip_already_imported = skip_already_imported @mapping = nil end @@ -31,21 +34,25 @@ class Import ActiveRecord::Base.no_touching do Current.with(account: account) do - Webhook.skip_callback(:create, :after, :create_delinquency_tracker!) - Comment.skip_callback(:commit, :after, :watch_card_by_creator) - Comment.skip_callback(:commit, :after, :track_creation) - Mention.skip_callback(:commit, :after, :watch_source_by_mentionee) - Notification.skip_callback(:commit, :after, :broadcast_unread) - Notification.skip_callback(:create, :after, :bundle) - Reaction.skip_callback(:create, :after, :register_card_activity) - Card.skip_callback(:save, :before, :set_default_title) - Card.skip_callback(:update, :after, :handle_board_change) - ActiveStorage::Blob.skip_callback(:update, :after, :touch_attachments) - ActiveStorage::Blob.skip_callback(:commit, :after, :update_service_metadata) - ActiveStorage::Attachment.skip_callback(:commit, :after, :mirror_blob_later) - ActiveStorage::Attachment.skip_callback(:commit, :after, :analyze_blob_later) - ActiveStorage::Attachment.skip_callback(:commit, :after, :transform_variants_later) - ActiveStorage::Attachment.skip_callback(:commit, :after, :purge_dependent_blob_later) + begin + Webhook.skip_callback(:create, :after, :create_delinquency_tracker!) + Comment.skip_callback(:commit, :after, :watch_card_by_creator) + Comment.skip_callback(:commit, :after, :track_creation) + Mention.skip_callback(:commit, :after, :watch_source_by_mentionee) + Notification.skip_callback(:commit, :after, :broadcast_unread) + Notification.skip_callback(:create, :after, :bundle) + Reaction.skip_callback(:create, :after, :register_card_activity) + Card.skip_callback(:save, :before, :set_default_title) + Card.skip_callback(:update, :after, :handle_board_change) + ActiveStorage::Blob.skip_callback(:update, :after, :touch_attachments) + ActiveStorage::Blob.skip_callback(:commit, :after, :update_service_metadata) + ActiveStorage::Attachment.skip_callback(:commit, :after, :mirror_blob_later) + ActiveStorage::Attachment.skip_callback(:commit, :after, :analyze_blob_later) + ActiveStorage::Attachment.skip_callback(:commit, :after, :transform_variants_later) + ActiveStorage::Attachment.skip_callback(:commit, :after, :purge_dependent_blob_later) + rescue => e + puts "⚠️ Warning: Could not skip some callbacks: #{e.message}" + end Event.suppress do copy_users @@ -81,6 +88,8 @@ class Import end puts "🎉 Import complete! (#{duration.round(2)}s)" + rescue AccountExistsError => e + raise e unless skip_already_imported end private @@ -115,7 +124,7 @@ class Import new_identity = Identity.find_or_create_by!(email_address: membership.identity.email_address) if Account.all.exists?(external_account_id: account.external_account_id) - raise "Account already exists" + raise AccountExistsError, "Account already exists" else @account = Account.create_with_admin_user( account: { @@ -542,8 +551,8 @@ class Import end Entropy.find_or_create_by!(account_id: account.id, container_type: old_entropy.container_type, container_id: container_id) do |entropy| - entropy.auto_postpone_period = old_entropy.auto_postpone_period, - entropy.created_at = old_entropy.created_at, + entropy.auto_postpone_period = old_entropy.auto_postpone_period || 0 + entropy.created_at = old_entropy.created_at entropy.updated_at = old_entropy.updated_at end end @@ -1290,10 +1299,20 @@ class Models end end -options = {} +options = { + skip_already_imported: false +} parser = OptionParser.new do |parser| - parser.banner = "Usage: #{$PROGRAM_NAME} " + parser.banner = "Usage: #{$PROGRAM_NAME} [options] ..." + + parser.on("--untenanted-db-path PATH", "Path to the untenanted database") do |path| + options[:untenanted_db_path] = path + end + + parser.on("--skip-already-imported", "Skip import if account already exists") do + options[:skip_already_imported] = true + end parser.on("-h", "--help", "Show this help message") do puts parser @@ -1303,20 +1322,40 @@ end parser.parse! -db_path = ARGV[0] -untenanted_db_path = ARGV[1] +untenanted_db_path = options[:untenanted_db_path] +tenanted_db_paths = ARGV -if db_path.nil? || untenanted_db_path.nil? - $stderr.puts "Error: both db_path and untenanted_db_path are required" +if untenanted_db_path.nil? + $stderr.puts "Error: --untenanted-db-path is required" $stderr.puts $stderr.puts parser exit 1 end -begin - Import.new(db_path, untenanted_db_path).import_database -rescue => e - $stderr.puts "Error: #{e.message}" - $stderr.puts e.backtrace.join("\n") if ENV["DEBUG"] +if tenanted_db_paths.empty? + $stderr.puts "Error: at least one tenanted database path is required" + $stderr.puts + $stderr.puts parser exit 1 end + +total_imported = 0 + +duration = ActiveSupport::Benchmark.realtime do + tenanted_db_paths.each_with_index do |db_path, index| + puts + puts "="*80 + puts "Processing database #{index + 1}/#{tenanted_db_paths.size}: #{db_path}" + puts "="*80 + + Import.new(db_path, untenanted_db_path, skip_already_imported: options[:skip_already_imported]).import_database + total_imported += 1 + end +end + +puts +puts "="*80 +puts "Summary:" +puts " Imported: #{total_imported}" +puts " Total time: #{duration.round(2)} seconds" +puts "="*80