Skip to content

v0.17.1 반복수 투영의 배포 실행 계약

추적: #1324, 원 변경 #1256, 릴리스 #1303.

상태: 두 기존 migration의 실제 CLI·4년 합성 fixture 검증을 완료했다. Production 배포 승인은 별도다. 실측 대상은 07c04f371b19c300524f2a404ababd26b3962609이며, 이후 추가되는 D02 등의 최종 릴리스 전체 검증은 #1324/#1303에 별도로 기록한다.

왜 별도 실행이 필요한가

20260913000000_reps_stats_projection_v1.sql은 열 추가와 전체 세부 세트 투영 UPDATE를 같은 트랜잭션에서 한다. 열 추가의 강한 테이블 잠금이 백필 종료까지 유지된다. 뒤의 20260913000100_reps_stats_readers_v1.sql은 함수 교체다. 두 파일은 이미 staging에 적용됐으므로 바꾸지 않는다.

Phase 1의 고정 이미지 실측은 첫 파일 5,357ms, 잠금 대기 5,017ms, 옛 앱 probe 최대 5,069ms였다. 53회 표본 중 47회가 Lock 대기를 봤고 오류는 0/15였다. 두 번째 파일은 258ms, 대기 0이었다. harness에 강제 5초 sleep/contention fixture는 없다. 원 측정 기록에 기록된 verdict=failed는 유지한다.

현재 sampler는 pg_stat_activity.state_change/query_start 경과를 대기시간으로 근사한다. 원 증거에 PID·blocker·pg_locks.waitstart는 없다. 따라서 정확한 wait 시작 시각을 소급 복원할 수 없고, 17ms를 빼서 합격시킬 수 없다. 후속 진단은 기존 5초 판정과 함께 실제 wait 시작·blocker를 기록해야 한다.

touch_completed_session_revision_from_child_v1은 원본 변경 없이 stats_reps만 새로 채워져도 부모 session.server_revision을 행마다 갱신한다. 기존 파생값 제외 목록이 effective_load·stats_effective_load_kg만 포함하기 때문이다. 기존 lift_guild.suppress_session_revision=on 실행 문맥은 이 중복 갱신을 막는다. 백필은 사용자 작성 내용을 바꾸지 않으므로 revision 문맥을 사용할 후보지만, 성능 개선과 옛 앱 CAS/캐시 의미론은 실측에서 확인해야 한다.

바꾸는 것과 보존 조건

scripts/migrations/release-v0171-projection-rollout.mjsSupabase CLI의 migration 파일 단위 트랜잭션·장부 기록을 그대로 사용한다. 새 SQL 실행 엔진이나 장부 수동 기록을 만들지 않는다.

  1. manifest의 base/head SHA, 해당 git diff의 신규 migration 목록, 파일별 SHA-256, 현재 worktree와 HEAD의 원문 일치를 확인한다. 과거 migration 변경·삭제·개명·추가 미등록 파일은 거부한다.
  2. 연결한 target의 CLI JSON dry-run에서 pending을 읽는다. 명시 manifest 전체 또는 이미 적용된 prefix 뒤의 정확한 suffix만 허용한다. 새 D02 등 후속 migration도 manifest에 이름과 hash를 적어야 한다.
  3. projection이 pending이면 임시 workdir에 과거 migration 전체와 projection 파일까지만 원문 byte 그대로 복사한다. 별도 dry-run이 projection 정확히 1개인지 확인한다.
  4. 이 workdir의 supabase db push 연결에만 URL startup parameter lift_guild.suppress_session_revision=on을 준다. 일반 trigger·원본 보호 trigger·투영 계산·dirty-date 표시는 유지한다. 원본 파일/예산/권한은 바꾸지 않는다.
  5. 첫 CLI가 종료된 뒤, suppression 없는 새 CLI 연결로 전체 workdir의 pending이 처음 목록에서 projection만 빠진 것인지 확인하고 남은 명시 migration을 적용한다. 마지막 dry-run은 빈 목록이어야 한다.
  6. 이미 projection이 적용된 staging이나 첫 단계 뒤 재개는 suppression 없이 나머지만 적용한다. 첫 단계 실패는 후속 단계를 실행하지 않는다. 임시 디렉터리는 finally에서 정리한다.

PGOPTIONS, PGSERVICE, PGSERVICEFILE은 자식 환경에서 제거하며 부모 환경은 수정하지 않는다. 설정은 DB role/database의 영구 기본값이나 다른 사용자 연결에 쓰지 않는다. 전체 릴리스에 공통 suppression을 거는 방식은 사용하지 않는다.

고정 원문 SHA-256:

파일SHA-256
20260913000000_reps_stats_projection_v1.sql7d7b98d93b919d07256294e2e73b78b5770c4618d00a75191fd9351e65f48299
20260913000100_reps_stats_readers_v1.sql03821d71f7ca19093ee6dcc6411f8d84778b3cbd814ac9b8130ae3ec623336c6

호출 입력

manifest는 승인할 release diff 전체를 고정한다. 다음 migrations 배열에는 실제 diff의 모든 신규 SQL 파일을 번호순으로 넣고 실제 hash를 적는다. 예시는 형식 설명이며 실행 가능한 승인 증거가 아니다.

json
{
  "version": 1,
  "baseSha": "직전 배포의 40자리 commit SHA",
  "headSha": "현재 checkout HEAD의 40자리 commit SHA",
  "migrations": [
    { "name": "20260913000000_reps_stats_projection_v1.sql", "sha256": "위 표의 hash" },
    { "name": "20260913000100_reps_stats_readers_v1.sql", "sha256": "위 표의 hash" }
  ]
}
sh
# 로컬 plan만: DB/자격 증명을 읽지 않는다.
node scripts/migrations/release-v0171-projection-rollout.mjs --manifest release.json --target-ref "$TARGET_REF"

# 이미 link 및 target/staging-applied/release-diff gate를 통과한 Deploy database job 안에서만:
node scripts/migrations/release-v0171-projection-rollout.mjs --manifest release.json --target-ref "$TARGET_REF" --apply

# 별도 로컬 리허설: linked 파일을 읽거나 고치지 않는다.
node scripts/migrations/release-v0171-projection-rollout.mjs --manifest release.json --sandbox /path/to/sandbox --apply

원격 실행은 .temp/project-ref, .temp/pooler-url, SUPABASE_DB_PASSWORD에서 같은 target의 연결 URL을 만든다. project-ref, Supabase 공식 host와 tenant username, postgres DB, 직접/세션 port 5432, TLS를 검사한다. URL의 임의 runtime 설정이나 트랜잭션 pooler 6543은 거부한다. 프로세스 출력에는 URL/비밀번호/원문 CLI 오류를 싣지 않는다. 실패 코드는 단계명과 exit code로 보고한다. 비밀번호를 명령 문자열이나 --debug 로그로 출력하지 않는다.

--sandbox는 그 sandbox의 config.toml에서 localhost port와 project_id를 읽고 supabase_db_<project_id>의 실제 포트·실행 상태·정본 PostgreSQL 이미지를 확인한다. 임의 DB URL/SQL/GUC를 받지 않는다. 증거의 env는 supabase-sandbox이고 Production 증거로 가장하지 않는다. DB reset·fixture 적재·probe는 caller의 별도 리허설 책임이다.

CLI 지원 근거

고정 Supabase CLI v2.113.0은 TypeScript/Bun + node-postgres 경로다. 과거 Go/pgx 설명만으로 현재 전달 경로를 추정하지 않는다.

  • CLI URL parser: URL의 runtime parameters와 options를 보존한다.
  • CLI 연결 계층: legacyMergedConnectionOptions가 runtime parameters를 startup -c key=value로 전달한다.
  • CLI migration 적용: 각 파일 전에 RESET ALL을 실행한다. 선행 SQL의 단순 SET은 따라서 대안이 아니다.
  • PostgreSQL 17 RESET: RESET은 runtime 설정을 세션의 기본값으로 돌린다. startup options로 준 값은 그 기본값의 원천이 될 수 있다. 실제 preset이 CLI 적용 중 유지되는지는 sandbox에서도 확인한다.

실제 CLI 2.113.0에서 구조화 출력은 --output-format json이다. --output json은 status 변수의 출력 형식이며 db push를 JSON으로 바꾸지 않는다. runner는 실제 CLI가 반환한 dryRun/migrations/seeds/roles JSON을 검사한다. --db-url 입력의 startup parameter가 migration 내부 UPDATE에서 on인 것을 statement-level audit로 확인했다.

2026-09-07 실제 CLI 검증

증거: 로컬 v0171-release-evidence/projection-rollout/{upgrade-evidence.json,preservation.json,interruption.json,normal-write.json,already-applied.json,definitions-check.json,environment.json}. 기준 v0.17.0의 172개 migration 위에 4년 합성 입력 1,043세션·12,516세부 세트·원본 33,127행을 저장 문으로 적재했다. 적재 중에만 cron을 격리한 뒤 원래 active 상태를 복구했다. 검증용 추가 trigger와 정상쓰기 트랜잭션은 끝난 뒤 제거/rollback했으며 fixture는 남겼다.

검증결과
실제 CLI projection 적용3,125ms. CLI 시작·연결 비용 포함
기존 sampler 기준 최대 잠금 / 실제 pg_locks.waitstart 기준1,498ms / 1,485ms. 5,000ms 예산 유지
readers 적용 / 잠금2,462ms / 0ms
옛 앱 probe오류 0/65, 실제 owner 호출 52, 미실행 migration 0
원본·ID·관계·출처23표 digest 42c93caeddbc 전후 동일
반복수 투영기록 유형을 적용한 12,516행 기대값과 전체 hash 동일; 불일치 0
무게 투영 / session revision전체 hash 보존. 1,043세션 revision 합 1,043으로 동일
dirty datesworker가 소진했던 0행에서 영향 날짜 1,043행이 다시 표시됨. suppression으로 dirty 경로를 끄지 않음
구/신 공존 / SQL 원천break 0. D13 public DDL/definitions 비교 차이 0(원천 객체 462, DB 객체 458)
실제 CLI 중단UPDATE 직전 sandbox fixture가 57014를 한 번 주입. 열 추가·데이터·revision·migration 장부 전부 rollback, 임시 workdir 정리 확인
정상 owner 저장새 연결의 suppression 없음. save_session_v5 revision 1→2, 동일 mutation 재생은 2 유지. 테스트 rollback 후 원본 fixture 보존
이미 적용된 재실행pending 0, 적용 phase 0. 설정 없는 정상 연결만 사용

실측은 사전 정한 실패주입 1회와 정상 적용 1회로 수행했다. 이전 5,017ms 실패는 다른 실행 환경의 증거로 보존하며 통과 숫자로 바꾸지 않았다. 현재 sandbox의 이미지 17.6.1.127, shared_buffers/effective_cache_size 각 128MB, 별도 CPU/memory 제한 없음, 자동 ANALYZE 완료 시각을 기록했다. 측정을 위해 캐시 예열·ANALYZE·자원 한도나 cron을 바꾸지 않았다. 따라서 개선 전후의 엄밀한 동일 조건 벤치마크나 Production 전체 데이터의 실측으로 해석하지 않는다.

schema:snapshot --check는 빈 replay 전용이므로 populated 사용자/통계 표를 보고 거부했다. 이를 불일치나 합격으로 바꾸지 않고 D13과 같은 public DDL/원천 모델 비교로 확인했다. 빈 DB replay와 최종 D02 포함 전체 검증은 별도 레인에서 수행한다.

필수 실측과 한계

기존 evidence는 Postgres 이미지·합성 4년 원본 33,127행·1,043세션·12,516세부 세트·오류 0을 확인했다. CPU 사용/할당·동시 컨테이너 부하·ANALYZE 완료 여부·buffer/cache 상태는 기록하지 않았다. G04 기준선의 별도 실험에는 공유 호스트의 컨테이너 약 130개가 언급되지만, 이를 D13의 실제 CPU 경합량으로 대체할 수 없다. 원 evidence의 주요 통계 투영 표도 0행이며, Production 기준선의 세부 세트 16,704행보다 작다. 그 증거만으로 Production 자원/전체 데이터 대표성이 충족됐다고 판단하지 않는다.

이 preset을 채택하려면 다음을 동일 후보 SHA로 기록한다.

  • 원문/preset의 사전 정한 비교 실험. 원 실패를 유지하고 통과할 때까지 재시도하지 않는다. 정확한 이미지·CPU/memory 한도·동시 부하·ANALYZE 및 cache 조건을 함께 기록한다.
  • 그날 데이터 사본 및 history-4y(필요시 10년 상한) populated 리허설. 실제 owner RLS RPC probe와 migration 중 writer 공존을 실행한다.
  • D13 예산: 파일 60초, 잠금 5,000ms, probe 오류 0. digest·canonical ID·간선·source identity 보존, 원천 일치, 공존 break 0. 이전 실패의 숫자를 고치지 않는다.
  • 원문과 preset의 stats_reps·무게 투영·dirty dates가 같은 결과인지 확인한다. 불필요한 server_revision 갱신은 줄어들되, 이후 실제 사용자 저장은 정확한 revision/receipt/generation을 발행하고 옛 탭의 CAS/읽기 캐시가 정상이어야 한다.
  • 첫 migration 중단 시 파일 전체 rollback 및 장부 미기록, 재개 성공. 첫 파일 commit 후 프로세스 중단 시 suppression 없는 후속 단계만 재개. 양쪽 단계 뒤 새 연결에는 suppression이 없다.
  • 실제 CLI preset을 쓰는 같은 실행 경로로 측정한다. psql에만 설정을 넣어 잰 결과를 CLI 배포의 증거로 쓰지 않는다.

이 경로도 잠금 예산을 넘으면 rollout은 보류한다. 뒤에 새 migration을 붙이거나 사후 배치만 추가해도 앞선 파일의 첫 잠금은 줄어들지 않는다. 사전 온라인 DDL/배치 방식은 원문이 전체 UPDATE를 다시 실행하고, 중간 스키마 공존·미완료 투영·재개 계약까지 새로 요구하므로 이 도구의 범위가 아니다. 예산을 확대하거나 이미 적용된 migration을 편집하는 방식으로 대체하지 않는다.