Skip to content

feat(scripts): D1→PostgreSQL 데이터 이전 스크립트 - #78

Merged
mack-erel merged 1 commit into
mainfrom
feat/d1-to-pg-migration
Jul 7, 2026
Merged

feat(scripts): D1→PostgreSQL 데이터 이전 스크립트#78
mack-erel merged 1 commit into
mainfrom
feat/d1-to-pg-migration

Conversation

@mack-erel

Copy link
Copy Markdown
Owner

D1(Cloudflare, SQLite 기반)에서 PostgreSQL로 운영 데이터를 이전하는 일회성 스크립트입니다. 스키마 마이그레이션(drizzle)이 아니라 행 데이터 이전을 다룹니다. 독립 코드 리뷰를 통과했으며(Critical/High 0), 기본 dry-run이라 실수 실행에도 PG를 변경하지 않습니다.

왜 안전한가

  • schema.sqlite.ts(=D1)와 schema.pg.ts는 테이블·컬럼·JS 키가 100% 대칭이라, 실제 값 변환은 **timestamp(정수 ms→Date)**와 boolean(0/1→bool) 두 종류뿐. 나머지(id·enum·JSON(text)·정수·모든 암호문/해시)는 무변환 복사.
  • 암호문·해시 컬럼(signing_keys.private_jwk_encrypted, TOTP credentials.secret, LDAP bind, audit_events.hash, 각종 토큰/비밀번호 해시)은 base64url/hex 문자열이라 IDP_SIGNING_KEY_SECRET만 동일 유지하면 그대로 이전 가능.

설계

  • 컬럼 종류를 drizzle 메타에서 자동 도출(하드코딩 아님 → 스키마 변경에 자동 추종)
  • snake_case(D1 raw) → camelCase(pg insert) 전 컬럼 매핑, 누락 없음
  • FK 부모→자식 순서(31테이블 1:1), departments 자기참조 위상정렬(사이클 가드)
  • rowid 커서 페이지네이션(대용량 audit_events 스트리밍), onConflictDoNothing 멱등
  • openScriptDb 재사용(scripts/lib/db.ts 무수정), 소스=d1·대상=postgres 이중 가드로 소스=대상 구조적 불가

옵션

--dry-run(기본, PG 미변경) · --apply · --skip-ephemeral(휘발성 테이블 생략) · --only= · --tables-exclude= · --batch-size= · --page-size= · --disable-fk

사용법

# 1) dry-run — 테이블별 scanned 건수만 집계
CLOUDFLARE_ACCOUNT_ID=… CLOUDFLARE_D1_DATABASE_ID=… CLOUDFLARE_API_TOKEN=… \
DATABASE_URL='postgres://…' bun scripts/migrate-d1-to-pg.ts

# 2) 실제 적용(+ 휘발성 테이블 생략)
… bun scripts/migrate-d1-to-pg.ts --apply --skip-ephemeral

실행 전 체크리스트

  1. IDP_SIGNING_KEY_SECRET가 D1 운영 때와 동일한가 (다르면 로그인/TOTP/서명 전부 깨짐)
  2. 대상 PG에 drizzle/pg 마이그레이션이 적용돼 스키마가 존재하는가
  3. DATABASE_URL이 의도한 대상(프리뷰/프로덕션)인가
  4. 먼저 dry-run으로 scanned 건수를 D1 실측과 대조 → --apply
  5. 적용 후 SELECT count(*) 대조 + 로그인/OIDC/SAML/TOTP 스모크 테스트

--skip-ephemeral이 생략하는 데이터(rate_limits·만료 토큰·challenges·grants·saml 임시)는 GC가 어차피 정리하므로 이전 불필요. audit_events는 보존 데이터라 항상 포함.

⚠️ 원격 D1 읽기/PG 쓰기 실행은 운영자가 직접 수행합니다(CLAUDE.md 규칙). 이 PR은 스크립트 추가만 포함.

🤖 Generated with Claude Code

- schema.sqlite(=D1)와 schema.pg의 대칭성 활용 — timestamp(정수 ms→Date)·boolean(0/1→bool)만 변환, 나머지(text/id/enum/암호문/해시)는 무변환 복사
- 암호문·해시 컬럼은 IDP_SIGNING_KEY_SECRET 동일 유지 시 그대로 이전 가능
- 컬럼 종류를 drizzle 메타에서 자동 도출(스키마 drift 자동 추종), snake_case→camelCase 매핑
- FK 부모→자식 순서(31테이블), departments 자기참조 위상정렬, onConflictDoNothing 멱등
- rowid 커서 페이지네이션(대용량 audit_events 스트리밍)
- 옵션: --dry-run(기본, PG 미변경)/--apply/--skip-ephemeral/--only/--tables-exclude/--disable-fk
- openScriptDb 재사용(db.ts 무수정), 소스=d1·대상=postgres 이중 가드
- 원격 DB 쓰기는 사용자가 직접 실행(CLAUDE.md 규칙) — 기본 dry-run

Co-Authored-By: Claude Fable 5 <[email protected]>
@mack-erel
mack-erel merged commit 3ee55d4 into main Jul 7, 2026
6 checks passed
@mack-erel
mack-erel deleted the feat/d1-to-pg-migration branch July 7, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant