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.mjs는 Supabase CLI의 migration 파일 단위 트랜잭션·장부 기록을 그대로 사용한다. 새 SQL 실행 엔진이나 장부 수동 기록을 만들지 않는다.
- manifest의 base/head SHA, 해당 git diff의 신규 migration 목록, 파일별 SHA-256, 현재 worktree와 HEAD의 원문 일치를 확인한다. 과거 migration 변경·삭제·개명·추가 미등록 파일은 거부한다.
- 연결한 target의 CLI JSON dry-run에서 pending을 읽는다. 명시 manifest 전체 또는 이미 적용된 prefix 뒤의 정확한 suffix만 허용한다. 새 D02 등 후속 migration도 manifest에 이름과 hash를 적어야 한다.
- projection이 pending이면 임시 workdir에 과거 migration 전체와 projection 파일까지만 원문 byte 그대로 복사한다. 별도 dry-run이 projection 정확히 1개인지 확인한다.
- 이 workdir의
supabase db push연결에만 URL startup parameterlift_guild.suppress_session_revision=on을 준다. 일반 trigger·원본 보호 trigger·투영 계산·dirty-date 표시는 유지한다. 원본 파일/예산/권한은 바꾸지 않는다. - 첫 CLI가 종료된 뒤, suppression 없는 새 CLI 연결로 전체 workdir의 pending이 처음 목록에서 projection만 빠진 것인지 확인하고 남은 명시 migration을 적용한다. 마지막 dry-run은 빈 목록이어야 한다.
- 이미 projection이 적용된 staging이나 첫 단계 뒤 재개는 suppression 없이 나머지만 적용한다. 첫 단계 실패는 후속 단계를 실행하지 않는다. 임시 디렉터리는 finally에서 정리한다.
PGOPTIONS, PGSERVICE, PGSERVICEFILE은 자식 환경에서 제거하며 부모 환경은 수정하지 않는다. 설정은 DB role/database의 영구 기본값이나 다른 사용자 연결에 쓰지 않는다. 전체 릴리스에 공통 suppression을 거는 방식은 사용하지 않는다.
고정 원문 SHA-256:
| 파일 | SHA-256 |
|---|---|
20260913000000_reps_stats_projection_v1.sql | 7d7b98d93b919d07256294e2e73b78b5770c4618d00a75191fd9351e65f48299 |
20260913000100_reps_stats_readers_v1.sql | 03821d71f7ca19093ee6dcc6411f8d84778b3cbd814ac9b8130ae3ec623336c6 |
호출 입력
manifest는 승인할 release diff 전체를 고정한다. 다음 migrations 배열에는 실제 diff의 모든 신규 SQL 파일을 번호순으로 넣고 실제 hash를 적는다. 예시는 형식 설명이며 실행 가능한 승인 증거가 아니다.
{
"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" }
]
}# 로컬 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 dates | worker가 소진했던 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을 편집하는 방식으로 대체하지 않는다.