Skip to content

Supabase 마이그레이션 전략

한국어 번역본

이 문서는 원문(영어)의 한국어 번역이다. 정본은 원문이며, 계약·게이트 판단이 갈리면 원문을 따른다. 원문: docs/data/supabase-migration-strategy.md

이 문서는 Barbelic이 수동으로 실행하던 수동 실행 patch SQL에서 추적되는 Supabase 마이그레이션으로 옮겨간 방식을 정의한다. 그 파일들은 이제 supabase/legacy/ 아래에 있으며, 각 파일 머리에 do-not-apply 배너가 붙어 있다(2026-08-20 RPC 표면 감사, SCR-4).

상태 스냅샷 (2026-09-04, 이슈 #1252)

supabase/schema.sqlsupabase/migrations/의 모든 파일을 빈 DB에 적용한 뒤의 상태 — 덤프이지 이력이 아니다. scripts/migrations/build-schema-snapshot.mjs(npm run schema:snapshot)가 만들고 손으로 고치지 않는다.

왜: 2차 기준선(2026-08-21) 뒤 2026-09-04까지 이 파일은 "마이그레이션을 버전 순으로 이어 붙인 것"으로 다시 만들어졌다. 그래서 다시 이력 대장이 됐다 — 152개 파일·8.2 MB, 뒤의 마이그레이션이 이미 대체한 함수 정의 359개(본문의 37%)가 남아 있었고, README는 "현재 상태"라고 약속하고 있었다. 아래 스냅샷은 재생한 DB에서 뽑으므로 이력으로 되돌아갈 수 없고, 세 가지 검사가 그것을 지킨다.

파일 구성(이 순서로):

  1. 머리 — 생성기 이름과 입력 지문: 모든 마이그레이션 파일 이름과 내용(LF 정규화)의 SHA-256. migrations-fingerprint가 이 스냅샷이 어느 마이그레이션 집합에서 나왔는지 묶어 준다.
  2. 스키마 — 재생한 DB의 pg_dump --schema-only --schema=public --quote-all-identifiers --no-owner를 2차 기준선이 쓴 멱등 형태로 다시 쓴 것(CREATE TABLE IF NOT EXISTS, CREATE [UNIQUE] INDEX IF NOT EXISTS, CREATE OR REPLACE FUNCTION/TRIGGER/VIEW, CREATE POLICY 앞에 DROP POLICY IF EXISTS, ADD CONSTRAINT 앞에 DROP CONSTRAINT IF EXISTS). 덤프 주석(-- Name: …)과 \restrict 줄은 지우고, 함수 본문 안의 -- 주석은 그대로 둔다. 확장은 pg_extension에서 뽑는다(plpgsql 제외).
  3. public 덤프가 담지 못하는 것auth.users 트리거, storage.buckets 행, storage.objects 정책, cron.job 행, public 스키마의 기본 권한 상태(pg_default_acl).
  4. 참조 데이터supabase/contracts/reference-data-tables.json에 적힌 표의 행. 표마다 insert … on conflict do nothing 하나, 행은 기본키 순(기본키가 INSERT에서 빠지는 열이면 — 예: exercise_synonyms.id — 실리는 열 전체 순), 모든 값은 열 타입으로 캐스트. 재생 시점에 DB가 값을 정하는 열은 재생마다 값이 달라지므로 뺀다 — 기본값이 시각·난수 함수(now(), clock_timestamp(), gen_random_uuid(), uuid_generate_v4())인 열, 시퀀스(nextval(…))·identity 열(exercise_strength_standards.id — 시드가 select … from jsonb 로 순서 없이 넣어 어느 행이 몇 번을 받는지 Linux CI와 Windows에서 달랐다). 기본값은 없지만 마이그레이션이 값으로 now()를 써 넣은 시각 열(예: exercise_strength_standards.cutlines_generated_at)도 같은 이유로 뺀다 — "재생 시작 시각"은 기본값 now() 열들의 최솟값이고, 그 이후 값이 한 행이라도 있는 시각 열이 대상이다. 목록에 없는 표에 행이 있으면 생성이 실패한다 — 목록이 "무엇이 참조 데이터인가"의 정본이고, 유저·운영 데이터 표는 절대 실리지 않는다.

생성기는 같은 DB에서 본문을 두 번 만들어 바이트가 같을 때만 쓴다. 덤프 순서가 흔들리면 파일이 출렁이는 대신 생성이 실패한다.

세 가지 검사:

검사도는 곳증명하는 것
tests/react/schemaSnapshotContract.test.mjsnpm test (Docker 없음)머리 지문 = 현재 supabase/migrations/의 지문, CRLF 없음, 같은 함수 시그니처가 두 번 정의되지 않음
build-schema-snapshot.mjs --checkCI migration-smokesupabase db reset 직후, npm run ci:local db 단계, npm run db:preflight재생한 DB에서 다시 만들면 커밋된 파일과 바이트까지 같다
npm run migrations:check / migrations:renumber랜딩개명 뒤 지문 머리가 따라간다(개명은 파일 이름·자기 주석을 바꾸므로 지문만 다시 계산해 써 넣는다 — 본문은 바뀌지 않는다)

마이그레이션을 추가·수정한 뒤의 절차: 샌드박스 스택을 띄우고(docs/process/ci-local.md) npm run schema:snapshot -- --sandbox <dir> --reset(거기에 마이그레이션을 재생하고 schema.sql을 다시 쓴다) → 둘을 함께 커밋. 맞지 않는 스냅샷은 npm run ci:local이 거부한다.

SQL 계약 테스트는 계속 tests/support/schemaSql.mjs(functionBody, policiesFor, seededTable, …)로 스냅샷을 읽는다. 파생 권한 대장과 마이그레이션 관용구 렌더링은 그대로다. 마이그레이션 이력은 supabase/migrations/와 git에 있고, 스냅샷은 이력을 하나도 싣지 않는다.

도메인 원천 (2026-09-07, 이슈 #1283, v0.18.0 D03)

스냅샷은 아무도 편집하지 않는 생성물이고 마이그레이션은 불변의 이력이라, D03 전에는 "함수 하나의 현재 정의를 열어서 읽고 고치는 파일"이 없었다. supabase/definitions/가 그 자리다: 스냅샷 1·2절을 객체 하나 = 파일 하나로 나눠 도메인 폴더에 둔다(<도메인>/functions/<이름>.sql, 제약·인덱스·정책·트리거·권한을 함께 담은 tables/<표>.sql, views/, 확장·스키마 설정·기본 권한·auth.users 트리거·storage·cron 의 db-platform/platform/ 파일 6개). 텍스트는 덤프 그대로이고 오버로드 함수는 인자 타입을 붙인 파일로 나뉜다. rules.json이 모든 객체의 도메인을 정하고(첫 일치 규칙, 미분류는 추출 실패), registry.json이 객체마다 qualified signature·층(door / engine / core / trigger)·보안·search_path·권한·의존·파일을 적고, exceptions.json은 첫 추출 시점의 층 규칙 위반(24건, 전부 호출 방향)을 닫힌 목록으로 든다.

네 가지가 항상 같고, 고리마다 검사가 있다.

같음검사
마이그레이션 → 재생 DBCI migration-smoke, npm run ci:local
재생 DB → schema.sqlbuild-schema-snapshot --check
schema.sqlsupabase/definitions/** + registry.jsonnpm run sql:check, tests/react/sqlDefinitionsContract.test.mjs(문장 집합 동일성, Docker 불필요)
층 규칙 ↔ exceptions.json같은 테스트 — 새 위반도, 낡은 예외도 실패

그래서 함수를 고치는 일은: 그 파일을 고친다 → npm run sql:candidate -- --slug <이름>(차이만 담은 마이그레이션 하나 — 함수 전체 본문·drop policy if exists + create policy·grant/revoke; 표·열·인덱스·제약·데이터 변경은 추측하지 않으므로 --ddl <파일>로 명시) → npm run schema:snapshot -- --sandbox <dir> --resetnpm run sql:extract(registry 갱신) → npm run sql:check. 리베이스 뒤에는 npm run sql:extract가 원천을 스냅샷에 수 초 만에 맞춘다. 정본: sql-definitions.md.

populated DB upgrade (2026-09-07, 이슈 #1287, v0.18.0 D13)

빈 DB 재생은 문법과 순서를 증명한다. 이미 유저 데이터가 쌓인 DB 에서 같은 파일이 얼마나 잠그고, 표를 다시 쓰는지, WAL 을 얼마나 쓰는지, 중간에 죽으면 어떻게 되는지, 직전 릴리스의 앱이 그동안 살아 있는지는 말하지 않는다. D13 은 그 물음에 답하는 세 조각을 더하고 기존 랜딩 게이트에 잇는다. 정본: populated-db-upgrade.md.

조각명령낸다
위험 분류npm run migrations:risk -- --write문장마다 여섯 축(잠금 모드·표 재작성·전체 스캔·WAL/디스크·재실행 안전·옛 앱 공존)의 부류와, 기계가 확인하는 헤더 한 줄 -- migration-risk: v1 level=… classes=… fingerprint=… — 지문은 실행 문장의 해시라 renumber 에는 그대로이고 편집에는 낡는다
upgrade harnessnpm run db:upgrade -- --from <직전 릴리스> --sandbox <dir> --fixture user-data.sql직전 릴리스 스키마를 격리 DB 에 만들고 fixture(매일 데이터 사본 또는 G04 workload)를 넣은 뒤 사실·canonical ID·부모/자식 간선·source identity 를 digest 하고, 대기 중인 마이그레이션을 파일당 한 트랜잭션으로 적용하는 동안 두 번째 세션이 옛 앱 RPC 를 부르고 세 번째 세션이 잠금 대기를 샘플링하며, 원하는 파일을 중간에 끊었다가 다시 적용하고, 다시 digest 하고, 두 ref 의 schema.sql 로 공존 break 를 계산해 upgrade-evidence.json-- upgrade-evidence: 줄을 쓴다
백필 primitivenpm run db:backfill -- --spec <spec.json>키셋 커서 배치(배치당 트랜잭션 하나, lock_timeout), 재개용 checkpoint 파일, 두 번째 러너를 막는 advisory lock, 어떤 행도 두 번 건드리지 않는 멱등 술어 — DB 객체는 하나도 만들지 않는다

게이트: check:migrations 는 규칙 도입 꼬리 뒤 번호의 모든 마이그레이션에 헤더를, level=high 면 증거 줄을 요구한다. db:preflightlanding:lock acquire 는 거기에 더해 증거가 Supabase 샌드박스(env=supabase-sandbox)에서 나왔을 것을 요구한다. 낮은 위험(함수·정책·권한·새 표)은 헤더 한 줄이면 끝이다. Docker 가 없는 곳에서 도구를 검증하는 bare Postgres shim(env=bare-postgres-shim)이 있지만 그 증거는 랜딩에 쓰지 못한다.

2차 기준선 (2026-08-21)

supabase/migrations/20260821000000_baseline_v2.sql20260622000100부터 20260820250000까지의 마이그레이션 125개를 한 파일로 대체했다. 이 파일은 그 마이그레이션들이 만들어낸 상태를 기록하며, 그것들을 재생하지 않는다.

왜 했는가: 로컬·CI 재생이 125개를 순차 실행해야 했고, schema.sql은 역사 전체를 이어붙인 7만 줄짜리 전사본이 되어 있었다. 나중 문장이 되돌린 문장까지 그대로 들어 있었고, 실제로 실행된 텍스트가 파일에 적힌 것이 아니라 마이그레이션 시점에 계산되던 곳이 40군데가 넘었다. 신규 기여자가 도면을 읽어도 DB를 알 수 없었다.

이 작업의 정의:

  • Production에 SQL을 실행하지 않는다. 유일한 Production 접촉은 supabase migration repair로 장부(supabase_migrations.schema_migrations)를 다시 쓰는 것이며, 이는 메타데이터다. 다운타임 0, 유저 데이터 무접촉.
  • 스키마 본문은 Production 덤프에서 뽑아 멱등 형태로 재작성했다 (create index if not exists, 정책 앞 drop policy if exists, 제약 앞 drop constraint if exists).
  • 참조 데이터(정식 카탈로그·archetype·근력 표준·추정 정책 버전)는 신선 재생이 만드는 행 집합에 Production의 을 실었다.
  • 덤프가 담지 못하는 6블록은 수동 선언했다: auth.users 트리거 2건, 스토리지 버킷 3개와 그 오브젝트 정책, pg_cron 잡 4건, 기본 권한 revoke, 그리고 check:policies가 요구하는 정책 출처 마커.
  • 이력은 삭제가 아니라 봉인이다. 원본 마이그레이션 전량이 git tag pre-squash-v2에 있다.

랜딩 전 실측한 파리티 증거: 빈 DB에 베이스라인만 재생한 결과와 Production 덤프의 차이는 문장 1,952개 중 2개뿐이고, 둘 다 기존 CHECK에 대한 PostgreSQL 자체의 괄호 표기 차이다. 객체 수(테이블 61·함수 227·정책 102·인덱스 208), cron 4건, 버킷 3개, auth 트리거 2건, 기본 ACL, 참조 15테이블 행수, 정식 카탈로그 md5가 전부 Production과 일치한다. 신선 재생에는 유저 행이 0이다.

스쿼시가 드러낸 드리프트

대조 과정에서 Production과 마이그레이션 전량 재생 사이의 차이 10건을 찾았다. 이 차이는 이 작업 이전부터 있었고, check:remote-schemamissing만 실패로 세고 제약 이름·on delete 동작·컬럼 순서·주석은 애초에 보지 않기 때문에 보이지 않았다. 베이스라인은 Production을 기록하므로 랜딩과 동시에 10건 전부 드리프트가 아니게 되지만, 그중 넷은 아무도 남길 의도가 없던 객체이고 랜딩 PR에 후속 폐기 대상으로 명시한다.

  • public.sets — 어떤 마이그레이션도 만들지 않는데 Production에 RLS 정책 4개를 달고 살아 있다. setsexercise_sets 개명 이전의 잔재.
  • user_exercise_pr_records의 개명 전 이름을 단 인덱스 2개.
  • 개명 전 이름으로 굳은 제약 7건(movements_pkey, movement_external_mappings_* 등) — alter table ... rename이 제약 이름을 따라 바꾸지 않기 때문.
  • exercise_archetypes.note — 어떤 마이그레이션도 추가하지 않는 컬럼.
  • exercises_archetype_id_fkey — 같은 이름, 다른 행동: 재생본은 on delete set null, Production은 동작 없음. 이 건만은 의미 차이라 정리가 아니라 판단이 필요하다.
  • 컬럼 순서가 8테이블 66컬럼에서 다르다. Production은 add column으로 붙였고 신선 재생은 create table 안에 선언하기 때문.
  • import_wodup_batch_to_canonical_engine(uuid)가 Production에서는 service_role로 실행 가능하고 재생본에서는 아무도 실행하지 못한다. 20260820130000_import_chain_verbatim_collapse.sql이 이 함수만 from public, anon, authenticated로 걷어냈는데, 형제 헬퍼 둘 (refresh_user_session_timing_stats_from, canonical_seoul_report_as_of_v1)은 service_role까지 함께 적는다. 호스티드 프로젝트의 기본 권한이 public 함수 실행을 service_role에 부여하므로, 이 누락은 내부 인입 엔진을 시크릿 키로 열어 둔 채로 남긴다. 로컬 CLI 스택에는 그 기본 권한이 없어서 로컬 실행은 계속 초록이었다. 렌더링 차이가 아니라 실제 구멍이며, 베이스라인은 Production을 기록해야 하므로 베이스라인 수정이 아니라 revoke 한 줄로 갚아야 한다. 재생성 시 재실측(2026-08-21): 같은 모양이 엔진 3종(stage_wodup_import_batch_engine, update_completed_session_v4_engine, delete_completed_session_v4_engine — 뒤 둘은 이슈 #1215(2026-09-04)로 삭제되고 save_session_v5_engine/delete_session_v5_engine이 대신한다)에도 있었고, main의 internal_surface_grant_hygiene.test.sql이 이를 단언하게 되어 상환은 스쿼시에 동승한 20260821000100_engine_grant_parity.sql이 수행한다.
  • Production이 재생본보다 넓은 함수 2건: refresh_wodup_complex_interpretations_v1()authenticated에도, training_effective_load(numeric,numeric,numeric)anon에도 부여되어 있다. 둘 다 정리가 아니라 판단 대상.

테스트에 무엇이 달라졌나

schema.sql이 더는 append-only가 아니므로, 나중 마이그레이션이 없앤 텍스트에 계약을 걸 수 없다. 그 부패는 실재했다: tests/react/schemaRls.test.mjs의 단언 61개가 이미 drop된 객체를 보며 통과하고 있었고, 그중에는 measured-PR 컷오버가 제거한 트리거도 있었다.

이제 SQL 계약은 tests/support/schemaSql.mjs를 통해 상태를 읽는다. 이 헬퍼는 매칭 전에 객체 하나로 범위를 좁히고, 덤프를 이 레포의 SQL 관용구로 렌더하며, 실효 권한을 마이그레이션이 쓰던 grant/revoke 쌍으로 파생시킨다. 손으로 유지하던 꼬리잠금 3종은 제네릭 계약 하나로 대체됐다 — schema.sql은 마이그레이션을 버전 순으로 이어붙인 것과 정확히 같다.

1차 기준선 (2026-06-22, 역사 기록)

아래 절들은 1차 기준선과 그 채택 절차의 기록이다. 장부 repair 절차와 멱등 규칙은 지금도 유효해서 남겨 둔다. 여기서 설명하는 4파일 분할은 2차 기준선에 접혔다.

현재 상태

  • 프로덕션 Supabase 프로젝트: kobxeylancdimqhfkbnl
  • 프로덕션 DB에는 중요한 패치 객체들이 이미 적용되어 있다.
  • 감사 시점에 supabase_migrations.schema_migrations를 사용할 수 없거나 볼 수 없었다. 그래서 DB에는 객체가 있지만 신뢰할 만한 마이그레이션 이력이 없다.

전환 규칙

  1. 오래된 패치 SQL 파일을 프로덕션에 다시 실행하지 않는다.
  2. 현재 supabase/schema.sql 상태를 기준선(baseline)으로 취급한다.
  3. 라이브 DB와 비교한 뒤, 그 기준선을 프로덕션에 적용된 것으로 등록한다.
  4. 기준선 이후의 모든 DB 변경에는 supabase/migrations/*.sql을 사용한다.
  5. supabase/legacy/*.sql는 역사 참고용으로만 유지하고, 절대 적용하지 않는다.

기준선 마이그레이션 분할

기준선은 schema.sql을 하나의 거대한 마이그레이션으로 복사하는 대신, 의존성 순서에 따라 의도적으로 분할한다.

순서파일역할
1supabase/migrations/20260622000100_lift_guild_baseline_schema.sql확장(extension), 스토리지 버킷 설정, 참조 데이터, 테이블, 컬럼, 제약, 인덱스
2supabase/migrations/20260622000110_lift_guild_baseline_operations.sql인입 파이프라인 함수, 통계 갱신 함수, 검증, 수리 유틸리티
3supabase/migrations/20260622000120_lift_guild_baseline_functions.sql앱을 마주하는 함수와 화면 RPC
4supabase/migrations/20260622000130_lift_guild_baseline_permissions.sqlgrant, revoke, RLS 활성화, 정책

이 순서는 의존성을 명시적으로 유지한다:

  • 함수가 테이블을 참조하기 전에 테이블이 존재한다.
  • 운영 함수를 호출하는 앱 쓰기 RPC보다 운영 함수가 먼저 존재한다.
  • grant와 RLS 정책이 함수를 참조하기 전에 함수가 존재한다.
  • 운영 유틸리티는 앱을 마주하는 RPC 계약과 분리된다.
  • 권한 변경이 마지막이므로, 객체 소유권과 접근 권한을 검토하기 쉽다.

기준선이 포괄하는 레거시 패치 범위

기준선은 다음 레거시 패치들의 최종 적용 상태를 나타낸다:

  • rename_movement_to_exercise_patch.sql
  • daily_conditions_patch.sql
  • exercises_patch.sql
  • exercises_admin_patch.sql
  • wodup_exercises_patch.sql
  • exercise_external_mappings_patch.sql
  • user_exercise_stats_patch.sql
  • write_rpc_patch.sql
  • private_user_data_rls_patch.sql
  • planned_sessions_rls_fix.sql
  • wodup_import_batches_patch.sql
  • wodup_import_staging_patch.sql
  • wodup_import_canonical_patch.sql
  • user_exercise_stats_integrity_patch.sql
  • wodup_placeholder_resolution_patch.sql
  • app_screen_rpc_patch.sql

다음 파일들은 기준선 마이그레이션이 아니라 운영 지원 스크립트로 남는다:

  • *_diagnostics.sql
  • *_preview.sql
  • *_confirm.sql
  • wodup_exercises_mapping_import_20260615/*
  • seed_dummy_account.sql

프로덕션 도입

프로덕션에는 이미 기준선 객체가 있다. 따라서 프로덕션 도입은 다음과 같다:

  1. 기준선 마이그레이션 파일을 생성한다.
  2. 새 로컬 DB에 대해 기준선을 검증한다.
  3. 프로덕션 객체를 기준선 결과와 비교한다.
  4. 네 개의 기준선 마이그레이션을 SQL을 다시 실행하지 않고 프로덕션에 적용됨으로 표시한다.
  5. 이후 마이그레이션만 Supabase 마이그레이션 도구로 적용한다.

프로덕션 기준선 수리 로그

2026-06-22에 연결된 프로젝트 kobxeylancdimqhfkbnl에 대해 적용했다.

네 개의 기준선 버전을 SQL 재실행 없이 적용됨으로 표시했다:

bash
npx supabase migration repair --linked --status applied \
  20260622000100 \
  20260622000110 \
  20260622000120 \
  20260622000130

검증:

bash
npx supabase migration list --linked
npx supabase db push --linked --dry-run

결과: 로컬과 원격 마이그레이션 이력이 일치하고, dry-run은 Remote database is up to date.를 보고한다.

기준선 검증 로그

2026-06-22에 검증을 실행했다.

이 Windows 작업 환경에서는 Supabase 로컬 스택용 Docker를 사용할 수 없거나 실행 중이 아니었기 때문에, 새 로컬 DB 검증을 완료할 수 없었다:

  • docker가 PATH에 없었다.
  • npx supabase statusnpx supabase db dump가 Docker 엔진에 연결하지 못했다.

대신 카탈로그 기반 점검으로 원격 검증을 완료했다:

bash
npm run check:remote-schema

최초 원격 카탈로그 비교에서 누락된 기준선 인덱스 네 개를 발견했다:

  • sessions_user_date_status_idx
  • planned_sessions_user_date_idx
  • planned_sessions_status_idx
  • planned_sets_planned_session_id_idx

다음으로 복구했다:

  • supabase/migrations/20260622000200_restore_missing_baseline_indexes.sql

그 마이그레이션을 적용한 뒤:

  • npx supabase migration list --linked는 로컬/원격 이력이 20260622000200까지 정렬되었음을 보여주었다.
  • npm run check:remote-schemamissing_count: 0을 보고했다.

원격 DB에는 여전히 기준선에 속하지 않는 잉여 레거시 객체가 있으며, public.sets와 관련 정책이 여기에 포함된다. 정리는 별도의 preview/confirm 운영 계획에서 처리해야 하므로, 이번 기준선 검증에서는 제거하지 않았다.

향후 마이그레이션 규칙

  • 가능한 곳에서는 멱등(idempotent) DDL을 선호한다:
    • create table if not exists
    • alter table ... add column if not exists
    • create index if not exists
    • create extension if not exists
    • create or replace function
  • RLS 정책은 본래 멱등하지 않다. 안전한 drop policy if exists
    • create policy 패턴이나 동등한 가드 블록을 사용한다.
  • add constraint는 본래 멱등하지 않다. 카탈로그 점검, drop constraint if exists, 또는 exception when duplicate_object then null로 가드한다.
  • 권한 변경을 같은 마이그레이션에 명시적으로 포함한다:
    • revoke all ... from public
    • revoke all ... from anon
    • 필요한 최소 역할에만 grant한다.
  • 화면 RPC는 BFF 계약으로 취급해야 한다. 프런트엔드 코드는 원시 테이블 레이아웃이 아니라 RPC 응답 형태에 의존해야 한다.
  • 데이터 수리 스크립트는 영구적인 스키마 변경이 아닌 한, preview/confirm 운영 SQL로 분리해 둬야 한다.
  • DB 마이그레이션 PR을 열거나 갱신하기 전에 npm run check:migrations를 실행한다.

2차 기준선 장부 repair

2차 기준선도 1차와 같은 방식으로 채택하되, 되돌리는 단계가 하나 붙는다. 대체된 버전들을 reverted로, 새 버전을 applied로 표시한다. SQL은 실행되지 않으며 메타데이터만 다시 쓴다.

bash
supabase migration repair --status reverted <대체된 버전>
supabase migration repair --status applied 20260821000000

검증은 supabase migration list --linked(로컬과 원격이 1:1로 맞고, 한쪽에만 있는 행이 없어야 한다)와 npm run check:remote-schema(EXIT=0)로 한다.

repair 전까지는 validateRemoteReleaseContract가 로컬 버전 목록과 원격 장부를 대조해 불일치를 보고한다. 랜딩과 repair 사이의 정상 상태이며, 이 계약은 npm run check 체인에 없으므로 CI를 막지 않는다.

롤백은 대칭이다 — 구버전을 applied로, 베이스라인을 reverted로 되돌리고 커밋을 revert한다. 이 절차는 Production 스키마를 건드리지 않으므로 데이터 리스크가 없다.

시작 전 반드시 참이어야 하는 조건 2가지, 둘 다 사고로 배운 것이다:

  • 원격 장부 행수와 로컬 마이그레이션 수가 같아야 한다. 마이그레이션은 PR 머지 전에 push되므로 Production이 main보다 몇 분 앞서는 구간이 상시로 있다. 그 창에서 뜬 베이스라인에는 아직 랜딩되지 않은 스키마가 섞인다.
  • supabase migration list --linked에 베이스라인 외의 로컬 전용 행이 없어야 한다. 적용에 실패한 마이그레이션은 큐 맨 앞에 남아 이후 모든 push를 막는다.

다음 단계

  1. Docker가 가능한 Supabase 로컬 스택을 쓸 수 있게 되면, 생성된 기준선 마이그레이션을 새 로컬 데이터베이스에서 검증한다.
  2. 워커 엔트리 포인트를 배포 가능하고 cron 준비된 상태로 유지한다. Wodup 인입은 이제 큐 적재에 wodup-start-import를, 무거운 처리에 wodup-process-import-jobs를 사용한다. 통계 갱신은 stats-process-refresh-jobs를 사용하며, 이것이 process_user_exercise_stats_refresh_jobs(...)를 호출한다.
  3. 앱 화면 RPC 계약과 응답 크기 가드레일을 강화한다.
  4. 낡은 수동 패치 참조는 모든 테스트와 문서가 더 이상 그것들에 의존하지 않게 된 뒤에만 제거하거나 보관 처리한다.