fix(db-migration): fix 2 changes with same ID
This solve issue with 2 migrations made the same day, due to merge on pull request, DB migration is blocking
the same version prefix:
- 20260625000000_files_user_size_index.sql (Dio)
- 20260625000000_folder_tree_modified_at.sql (Ed)
They were renamed to ...0001 and ...0002 (disjoint versions) + protection like "IF NOT EXISTS"
I have opt for an automated clean up of old entry:
`DELETE FROM _sqlx_migrations WHERE version = 20260625000000;`
runned on startup
affected users: Dio, myself and any dev that wanted to work on this project since eb0ba58158
This commit is contained in:
+38
-5
@@ -14,15 +14,43 @@
|
|||||||
-- chain on every file write and every folder mutation. Performance
|
-- chain on every file write and every folder mutation. Performance
|
||||||
-- ceiling: O(depth) row updates per mutation; deep concurrent writes
|
-- ceiling: O(depth) row updates per mutation; deep concurrent writes
|
||||||
-- to the same root subtree can contend on the root row.
|
-- to the same root subtree can contend on the root row.
|
||||||
|
--
|
||||||
|
-- Idempotency note: this migration was originally numbered
|
||||||
|
-- `20260625000000` and collided with `files_user_size_index` from a
|
||||||
|
-- parallel branch. Both files were renamed to disjoint versions, and
|
||||||
|
-- this body uses `IF NOT EXISTS` / `CREATE OR REPLACE` /
|
||||||
|
-- `DROP TRIGGER IF EXISTS` throughout so the migration is safe to
|
||||||
|
-- re-run against databases that already applied it under the old
|
||||||
|
-- version number. An orphan row for `20260625000000` may remain in
|
||||||
|
-- `_sqlx_migrations` on such databases — sqlx ignores rows whose
|
||||||
|
-- version no longer maps to a source file.
|
||||||
|
|
||||||
|
-- Gate the entire column-add + backfill block on column absence.
|
||||||
|
-- A naive `ADD COLUMN IF NOT EXISTS` paired with an unconditional
|
||||||
|
-- backfill `UPDATE` would clobber trigger-bumped values back to
|
||||||
|
-- `updated_at` on databases that already deployed this migration
|
||||||
|
-- under the old `20260625000000` version — every folder NC clients
|
||||||
|
-- have synced since first deploy would suddenly look "modified",
|
||||||
|
-- triggering a one-time re-walk. The DO block keeps the migration
|
||||||
|
-- a true no-op for those databases: column exists → skip both
|
||||||
|
-- statements → preserve the live trigger-maintained values.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'storage'
|
||||||
|
AND table_name = 'folders'
|
||||||
|
AND column_name = 'tree_modified_at'
|
||||||
|
) THEN
|
||||||
ALTER TABLE storage.folders
|
ALTER TABLE storage.folders
|
||||||
ADD COLUMN tree_modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
ADD COLUMN tree_modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||||
|
-- Backfill on first-deploy: collapse to per-folder updated_at.
|
||||||
-- Backfill existing rows: collapse the rollup timestamp to the
|
-- Clients re-walking after deploy will see one batch of
|
||||||
-- per-folder updated_at. Clients re-walking after deploy will see
|
-- "looks new to me" responses, handled as
|
||||||
-- one batch of "looks new to me" responses, which they handle as a
|
-- content-match-no-download — the expected one-time resync.
|
||||||
-- content-match-no-download — the expected one-time resync wave.
|
|
||||||
UPDATE storage.folders SET tree_modified_at = updated_at;
|
UPDATE storage.folders SET tree_modified_at = updated_at;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
-- File-side trigger: any INSERT/UPDATE/DELETE on storage.files
|
-- File-side trigger: any INSERT/UPDATE/DELETE on storage.files
|
||||||
@@ -60,6 +88,10 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
-- PG 13 doesn't support `CREATE OR REPLACE TRIGGER` (added in PG 14),
|
||||||
|
-- so use the DROP-then-CREATE pattern to stay re-runnable on the
|
||||||
|
-- minimum supported version.
|
||||||
|
DROP TRIGGER IF EXISTS files_bump_folder_tree_etag ON storage.files;
|
||||||
CREATE TRIGGER files_bump_folder_tree_etag
|
CREATE TRIGGER files_bump_folder_tree_etag
|
||||||
AFTER INSERT OR UPDATE OR DELETE ON storage.files
|
AFTER INSERT OR UPDATE OR DELETE ON storage.files
|
||||||
FOR EACH ROW EXECUTE FUNCTION storage.bump_folder_tree_from_file();
|
FOR EACH ROW EXECUTE FUNCTION storage.bump_folder_tree_from_file();
|
||||||
@@ -99,6 +131,7 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS folders_bump_folder_tree_etag ON storage.folders;
|
||||||
CREATE TRIGGER folders_bump_folder_tree_etag
|
CREATE TRIGGER folders_bump_folder_tree_etag
|
||||||
AFTER INSERT OR UPDATE OR DELETE ON storage.folders
|
AFTER INSERT OR UPDATE OR DELETE ON storage.folders
|
||||||
FOR EACH ROW EXECUTE FUNCTION storage.bump_folder_tree_from_folder();
|
FOR EACH ROW EXECUTE FUNCTION storage.bump_folder_tree_from_folder();
|
||||||
@@ -163,6 +163,35 @@ async fn create_pool_with_retries(
|
|||||||
/// in a `_sqlx_migrations` table. Each migration runs in its own transaction.
|
/// in a `_sqlx_migrations` table. Each migration runs in its own transaction.
|
||||||
/// Migration files are embedded at compile time via `sqlx::migrate!()`.
|
/// Migration files are embedded at compile time via `sqlx::migrate!()`.
|
||||||
async fn run_migrations(pool: &PgPool) -> Result<()> {
|
async fn run_migrations(pool: &PgPool) -> Result<()> {
|
||||||
|
// ── One-time pre-flight cleanup for the 20260625000000 collision ──
|
||||||
|
//
|
||||||
|
// Two migrations landed on the same day from parallel branches with
|
||||||
|
// the same version prefix:
|
||||||
|
// - 20260625000000_files_user_size_index.sql (Dio)
|
||||||
|
// - 20260625000000_folder_tree_modified_at.sql (Ed)
|
||||||
|
// They were renamed to ...0001 and ...0002 (disjoint versions), and
|
||||||
|
// both bodies were made idempotent so they re-run safely against
|
||||||
|
// databases that already applied either original under the shared
|
||||||
|
// version. However sqlx 0.8's default strict mode errors on boot
|
||||||
|
// when `_sqlx_migrations` contains a row whose version no longer
|
||||||
|
// maps to a source file ("previously applied but is missing in the
|
||||||
|
// resolved migrations") — which is exactly the state of every
|
||||||
|
// contributor DB that booted before the rename.
|
||||||
|
//
|
||||||
|
// This DELETE silently clears that stale bookkeeping row. The
|
||||||
|
// schema effects of whichever original ran are preserved
|
||||||
|
// (idempotent re-application via ...0001 / ...0002 is a no-op on
|
||||||
|
// already-modified schemas). On fresh databases the table doesn't
|
||||||
|
// exist yet, the query errors, and the `let _` swallows it —
|
||||||
|
// sqlx::migrate!() then creates the table cleanly on its first
|
||||||
|
// pass.
|
||||||
|
//
|
||||||
|
// Sunset: drop this block once the contributor base has rolled
|
||||||
|
// past the affected commit window. Suggested review date 2026-12.
|
||||||
|
let _ = sqlx::query("DELETE FROM _sqlx_migrations WHERE version = 20260625000000")
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
match sqlx::migrate!().run(pool).await {
|
match sqlx::migrate!().run(pool).await {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(e) => Err(DbError(format!("Migration error: {}", e))),
|
Err(e) => Err(DbError(format!("Migration error: {}", e))),
|
||||||
|
|||||||
Reference in New Issue
Block a user