mega-framework 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* PostgreSQL dialect (ADR-204) — differ 가 산출한 변경 연산(op)을 SQL 문자열(up/down 쌍)로 렌더한다.
|
|
4
|
+
*
|
|
5
|
+
* # 명명 표준 (diff 안정성의 핵심 — 이름이 흔들리면 변경이 drop+add 로 오판된다)
|
|
6
|
+
* - 인덱스: `idx_<table>_<col1>_<col2>` (UNIQUE 인덱스: `uniq_<table>_<cols>`)
|
|
7
|
+
* - UNIQUE 제약: `uniq_<table>_<col>`
|
|
8
|
+
* - FK: `fk_<table>_<col>_<reftable>`
|
|
9
|
+
* - CHECK: `chk_<table>_<col>` (사용자 지정 name 우선)
|
|
10
|
+
* - PRIMARY KEY(복합): `pk_<table>`
|
|
11
|
+
*
|
|
12
|
+
* # 렌더 원칙
|
|
13
|
+
* - 식별자는 필요할 때만 따옴표(`"camelCase"` 등) — 소문자 snake 는 bare 로 가독성 유지.
|
|
14
|
+
* - enum 은 `TEXT` + `CHECK (col IN (...))` — native enum 타입 미사용(값 변경이 제약 교체로 단순).
|
|
15
|
+
* - FK 는 CREATE TABLE 인라인이 아니라 **별도 ADD CONSTRAINT** — 같은 마이그레이션 안에서
|
|
16
|
+
* 상호 참조하는 새 테이블들의 생성 순서 문제를 제거한다(differ 가 createTable 뒤에 addFk 를 배치).
|
|
17
|
+
* - 파괴적 변경(DROP TABLE/COLUMN)·위험 캐스트(ALTER TYPE)는 SQL 안에 경고/TODO 주석을 박는다
|
|
18
|
+
* (silent 손실 금지 — 사용자가 적용 전 검토).
|
|
19
|
+
*
|
|
20
|
+
* @module core/migration/dialects/postgres
|
|
21
|
+
*/
|
|
22
|
+
import { MegaConfigError } from '../../../errors/config-error.js'
|
|
23
|
+
|
|
24
|
+
// ── dialect 표준 인터페이스 속성 (ADR-205 — dialects/README.md 가 contract 정본) ─────────────
|
|
25
|
+
/** 식별자 최대 길이(byte) — postgres NAMEDATALEN-1. 초과는 절단·충돌 위험이라 fail-fast(정책 2). */
|
|
26
|
+
export const identifierMaxBytes = 63
|
|
27
|
+
/** CREATE INDEX CONCURRENTLY 지원(트랜잭션 밖 실행 필요 — generate --concurrent 옵트인). */
|
|
28
|
+
export const supportsConcurrentIndex = true
|
|
29
|
+
/** enum 표현 전략 — 'check' = TEXT + CHECK(col IN (...)). (maria 는 'enum-type' 예정.) */
|
|
30
|
+
export const enumStrategy = 'check'
|
|
31
|
+
/** RENAME COLUMN 의 트랜잭션 내 실행 지원. */
|
|
32
|
+
export const supportsRenameInTx = true
|
|
33
|
+
/** DROP COLUMN 의 트랜잭션 내 롤백 가능(maria 는 DDL 암묵 commit 으로 false 예정). */
|
|
34
|
+
export const canDropColumnInTx = true
|
|
35
|
+
/** 일부 ALTER 가 테이블 재생성을 요구하는지(sqlite true 예정). */
|
|
36
|
+
export const dependsOnRebuild = false
|
|
37
|
+
/** ALTER TABLE ... RENAME CONSTRAINT 지원(postgres 9.2+). 미지원 dialect 는 DROP+ADD 로 대체. */
|
|
38
|
+
export const supportsRenameConstraint = true
|
|
39
|
+
/** ALTER 로 FK 추가/삭제 가능 — false(sqlite)면 FK 는 CREATE TABLE 인라인 + 변경은 rebuild. */
|
|
40
|
+
export const supportsAlterAddFk = true
|
|
41
|
+
/** FK 걸린 컬럼 DROP 전 dropFk 선행 필요 여부 — postgres 는 의존 객체 cascade 라 불필요. */
|
|
42
|
+
export const requiresDropFkBeforeDropColumn = false
|
|
43
|
+
/** SQL DDL 렌더 dialect — 생성 파일이 `db.query(sql)` 계약을 쓴다(mongo 와 분기, ADR-209). */
|
|
44
|
+
export const usesSqlDdl = true
|
|
45
|
+
|
|
46
|
+
/** bare 로 둘 수 있는 식별자 — 그 외(camelCase·예약어 충돌 가능성)는 따옴표. */
|
|
47
|
+
const BARE_IDENT_RE = /^[a-z_][a-z0-9_]*$/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* SQL 식별자 인용 — 필요할 때만 `"..."`. **모든 식별자가 지나는 단일 깔때기**라 여기서
|
|
51
|
+
* 63byte 초과를 fail-fast 한다 — postgres 는 NOTICE 와 함께 조용히 절단해 이름 충돌·
|
|
52
|
+
* `migrate:status` 표기 불일치를 만들기 때문(자동 축약 없이 거부, 정책 2).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name @returns {string}
|
|
55
|
+
* @throws {MegaConfigError} `migration.identifier_too_long` - 63byte 초과(합성 이름 포함).
|
|
56
|
+
*/
|
|
57
|
+
export function quoteIdent(name) {
|
|
58
|
+
if (Buffer.byteLength(name, 'utf8') > identifierMaxBytes) {
|
|
59
|
+
throw new MegaConfigError(
|
|
60
|
+
'migration.identifier_too_long',
|
|
61
|
+
`식별자 '${name}' 가 postgres 한도(${identifierMaxBytes}byte)를 초과합니다 — 절단 시 이름 충돌 위험이 있어 거부합니다. ` +
|
|
62
|
+
'테이블/컬럼 이름을 줄이거나, 합성 이름(idx_/fk_ 등)이 문제라면 .unique({name})/.references(...,{name})/t.index(...,{name}) 으로 짧은 이름을 명시하세요.',
|
|
63
|
+
{ details: { identifier: name, bytes: Buffer.byteLength(name, 'utf8'), max: identifierMaxBytes } },
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
if (BARE_IDENT_RE.test(name)) return name
|
|
67
|
+
return `"${name.replaceAll('"', '""')}"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* DEFAULT/CHECK 용 literal 렌더 — 문자열은 단일따옴표 escape, `{ raw }` 는 검증된 식 그대로.
|
|
72
|
+
* @param {unknown} value @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function renderLiteral(value) {
|
|
75
|
+
if (value === null) return 'NULL'
|
|
76
|
+
if (typeof value === 'number') {
|
|
77
|
+
// NaN/Infinity 는 SQL literal 이 아니다 — invalid SQL 을 만들기 전에 거부.
|
|
78
|
+
if (!Number.isFinite(value)) {
|
|
79
|
+
throw new MegaConfigError('migration.schema_invalid', `default 값이 유한한 수가 아닙니다: ${value}`, { details: { value: String(value) } })
|
|
80
|
+
}
|
|
81
|
+
return String(value)
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'
|
|
84
|
+
if (typeof value === 'object' && value !== null && typeof (/** @type {any} */ (value).raw) === 'string') {
|
|
85
|
+
return /** @type {any} */ (value).raw
|
|
86
|
+
}
|
|
87
|
+
return `'${String(value).replaceAll("'", "''")}'`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** dialect 표준 인터페이스 별칭 — 값 인용(SQL injection 방지 깔때기). @type {typeof renderLiteral} */
|
|
91
|
+
export const quoteLiteral = renderLiteral
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 컬럼 타입 → postgres SQL 타입.
|
|
95
|
+
* @param {Record<string, any>} def @param {{ forAlter?: boolean }} [opts] - forAlter: ALTER TYPE 대상
|
|
96
|
+
* (SERIAL 은 타입이 아니라 시퀀스 단축문법이라 INTEGER/BIGINT 로 강등).
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function typeSql(def, { forAlter = false } = {}) {
|
|
100
|
+
switch (def.type) {
|
|
101
|
+
case 'serial':
|
|
102
|
+
return forAlter ? 'INTEGER' : 'SERIAL'
|
|
103
|
+
case 'bigSerial':
|
|
104
|
+
return forAlter ? 'BIGINT' : 'BIGSERIAL'
|
|
105
|
+
case 'integer':
|
|
106
|
+
return 'INTEGER'
|
|
107
|
+
case 'bigInteger':
|
|
108
|
+
return 'BIGINT'
|
|
109
|
+
case 'smallInteger':
|
|
110
|
+
return 'SMALLINT'
|
|
111
|
+
case 'real':
|
|
112
|
+
return 'REAL'
|
|
113
|
+
case 'doublePrecision':
|
|
114
|
+
return 'DOUBLE PRECISION'
|
|
115
|
+
case 'decimal':
|
|
116
|
+
return `NUMERIC(${def.precision}, ${def.scale})`
|
|
117
|
+
case 'varchar':
|
|
118
|
+
return `VARCHAR(${def.length})`
|
|
119
|
+
case 'text':
|
|
120
|
+
return 'TEXT'
|
|
121
|
+
case 'char':
|
|
122
|
+
return `CHAR(${def.length})`
|
|
123
|
+
case 'boolean':
|
|
124
|
+
return 'BOOLEAN'
|
|
125
|
+
case 'timestamp':
|
|
126
|
+
return 'TIMESTAMP'
|
|
127
|
+
case 'timestamptz':
|
|
128
|
+
return 'TIMESTAMPTZ'
|
|
129
|
+
case 'date':
|
|
130
|
+
return 'DATE'
|
|
131
|
+
case 'time':
|
|
132
|
+
return 'TIME'
|
|
133
|
+
case 'uuid':
|
|
134
|
+
return 'UUID'
|
|
135
|
+
case 'json':
|
|
136
|
+
return 'JSON'
|
|
137
|
+
case 'jsonb':
|
|
138
|
+
return 'JSONB'
|
|
139
|
+
case 'enum':
|
|
140
|
+
return 'TEXT'
|
|
141
|
+
case 'bytea':
|
|
142
|
+
return 'BYTEA'
|
|
143
|
+
default:
|
|
144
|
+
throw new MegaConfigError('migration.schema_invalid', `postgres dialect: 알 수 없는 타입 '${def.type}'.`, { details: { type: def.type } })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 명명 표준 ────────────────────────────────────────────────────────────────
|
|
149
|
+
/** @param {string} table @param {string[]} cols @param {boolean} [unique] @returns {string} */
|
|
150
|
+
export function indexName(table, cols, unique = false) {
|
|
151
|
+
return `${unique ? 'uniq' : 'idx'}_${table}_${cols.join('_')}`
|
|
152
|
+
}
|
|
153
|
+
/** @param {string} table @param {Record<string, any>} ix @returns {string} 인덱스 정의의 유효 이름. */
|
|
154
|
+
export function resolveIndexName(table, ix) {
|
|
155
|
+
if (ix.name !== undefined) return ix.name
|
|
156
|
+
if (ix.expression !== undefined) {
|
|
157
|
+
throw new MegaConfigError('migration.schema_invalid', `표현식 인덱스는 name 이 필수입니다 (table '${table}', expression '${ix.expression}').`, {
|
|
158
|
+
details: { table, expression: ix.expression },
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
return indexName(table, ix.columns, ix.unique === true)
|
|
162
|
+
}
|
|
163
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
164
|
+
export function uniqueName(table, col) {
|
|
165
|
+
return `uniq_${table}_${col}`
|
|
166
|
+
}
|
|
167
|
+
/** @param {string} table @param {string} col @param {string} refTable @returns {string} */
|
|
168
|
+
export function fkName(table, col, refTable) {
|
|
169
|
+
return `fk_${table}_${col}_${refTable}`
|
|
170
|
+
}
|
|
171
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
172
|
+
export function checkName(table, col) {
|
|
173
|
+
return `chk_${table}_${col}`
|
|
174
|
+
}
|
|
175
|
+
/** @param {string} table @returns {string} */
|
|
176
|
+
export function pkName(table) {
|
|
177
|
+
return `pk_${table}`
|
|
178
|
+
}
|
|
179
|
+
/** CREATE TABLE 인라인 단일 PK 의 서버 자동 명명. @param {string} table @returns {string} */
|
|
180
|
+
export function inlinePkName(table) {
|
|
181
|
+
return `${table}_pkey`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* enum 컬럼의 CHECK 식 — **유일한 enum CHECK 렌더 지점**(differ 는 values 만 전달, ADR-205 H-1).
|
|
186
|
+
* @param {string} col @param {string[]} values @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
export function enumCheckExpr(col, values) {
|
|
189
|
+
return `${quoteIdent(col)} IN (${values.map((v) => renderLiteral(v)).join(', ')})`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 컬럼 정의 조각(타입 + NOT NULL + DEFAULT [+ PRIMARY KEY]) — CREATE TABLE/ADD COLUMN 공용.
|
|
194
|
+
* unique/check/fk 는 별도 제약 연산으로 렌더한다(명명 표준 유지·드롭 가능).
|
|
195
|
+
* @param {string} name @param {Record<string, any>} def @param {{ inlinePrimary?: boolean }} [opts]
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function columnClause(name, def, { inlinePrimary = true } = {}) {
|
|
199
|
+
let sql = `${quoteIdent(name)} ${typeSql(def)}`
|
|
200
|
+
if (def.notNull === true) sql += ' NOT NULL'
|
|
201
|
+
if (def.default !== undefined) sql += ` DEFAULT ${renderLiteral(def.default)}`
|
|
202
|
+
if (inlinePrimary && def.primary === true) sql += ' PRIMARY KEY'
|
|
203
|
+
return sql
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 한 컬럼의 부속 제약(UNIQUE/CHECK/enum CHECK) ADD CONSTRAINT 문 목록 — FK 는 differ 가 별도 op 로.
|
|
208
|
+
* @param {string} table @param {string} col @param {Record<string, any>} def
|
|
209
|
+
* @returns {string[]}
|
|
210
|
+
*/
|
|
211
|
+
function columnConstraintStatements(table, col, def) {
|
|
212
|
+
/** @type {string[]} */
|
|
213
|
+
const out = []
|
|
214
|
+
if (def.unique !== undefined) {
|
|
215
|
+
const name = def.unique === true ? uniqueName(table, col) : def.unique.name
|
|
216
|
+
out.push(`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} UNIQUE (${quoteIdent(col)})`)
|
|
217
|
+
}
|
|
218
|
+
if (def.type === 'enum') {
|
|
219
|
+
const name = def.enumName ?? checkName(table, col)
|
|
220
|
+
out.push(`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} CHECK (${enumCheckExpr(col, def.values)})`)
|
|
221
|
+
}
|
|
222
|
+
if (def.check !== undefined) {
|
|
223
|
+
const name = def.check.name ?? checkName(table, col)
|
|
224
|
+
out.push(`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} CHECK (${def.check.expr})`)
|
|
225
|
+
}
|
|
226
|
+
return out
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** FK ADD CONSTRAINT 문. @param {string} table @param {string} col @param {Record<string, any>} ref @returns {string} */
|
|
230
|
+
function fkAddStatement(table, col, ref) {
|
|
231
|
+
const name = ref.name ?? fkName(table, col, ref.table)
|
|
232
|
+
let sql = `ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} FOREIGN KEY (${quoteIdent(col)}) REFERENCES ${quoteIdent(ref.table)} (${quoteIdent(ref.column)})`
|
|
233
|
+
if (ref.onDelete !== undefined) sql += ` ON DELETE ${ref.onDelete.toUpperCase()}`
|
|
234
|
+
if (ref.onUpdate !== undefined) sql += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`
|
|
235
|
+
return sql
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** FK DROP CONSTRAINT 문. @param {string} table @param {string} col @param {Record<string, any>} ref @returns {string} */
|
|
239
|
+
function fkDropStatement(table, col, ref) {
|
|
240
|
+
return `ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(ref.name ?? fkName(table, col, ref.table))}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* CREATE INDEX 문. concurrent=true 면 `CONCURRENTLY`(쓰기 비블로킹 — 단 트랜잭션 밖 실행 필요,
|
|
245
|
+
* generate --concurrent 가 no-tx 마이그레이션으로 묶는다).
|
|
246
|
+
* @param {string} table @param {Record<string, any>} ix @param {{ concurrent?: boolean }} [opts]
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function indexCreateStatement(table, ix, { concurrent = false } = {}) {
|
|
250
|
+
const name = resolveIndexName(table, ix)
|
|
251
|
+
const target = ix.expression !== undefined ? `(${ix.expression})` : `(${ix.columns.map(quoteIdent).join(', ')})`
|
|
252
|
+
let sql = `CREATE ${ix.unique === true ? 'UNIQUE ' : ''}INDEX ${concurrent ? 'CONCURRENTLY ' : ''}${quoteIdent(name)} ON ${quoteIdent(table)}`
|
|
253
|
+
if (ix.using !== undefined) sql += ` USING ${ix.using}`
|
|
254
|
+
sql += ` ${target}`
|
|
255
|
+
if (ix.where !== undefined) sql += ` WHERE ${ix.where}`
|
|
256
|
+
return sql
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** COMMENT ON COLUMN 문. @param {string} table @param {string} col @param {string | null} text @returns {string} */
|
|
260
|
+
function commentStatement(table, col, text) {
|
|
261
|
+
return `COMMENT ON COLUMN ${quoteIdent(table)}.${quoteIdent(col)} IS ${text === null ? 'NULL' : renderLiteral(text)}`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* CREATE TABLE 문(+부속 제약/코멘트) — FK·인덱스 제외(별도 op).
|
|
266
|
+
* @param {Record<string, any>} record
|
|
267
|
+
* @returns {string[]}
|
|
268
|
+
*/
|
|
269
|
+
function createTableStatements(record) {
|
|
270
|
+
const table = record.table
|
|
271
|
+
/** @type {string[]} */
|
|
272
|
+
const lines = []
|
|
273
|
+
for (const [name, def] of Object.entries(record.columns)) {
|
|
274
|
+
lines.push(' ' + columnClause(name, def))
|
|
275
|
+
}
|
|
276
|
+
if (record.primaryKey !== undefined) {
|
|
277
|
+
lines.push(` CONSTRAINT ${quoteIdent(pkName(table))} PRIMARY KEY (${record.primaryKey.map(quoteIdent).join(', ')})`)
|
|
278
|
+
}
|
|
279
|
+
/** @type {string[]} */
|
|
280
|
+
const out = [`CREATE TABLE ${quoteIdent(table)} (\n${lines.join(',\n')}\n)`]
|
|
281
|
+
for (const [name, def] of Object.entries(record.columns)) {
|
|
282
|
+
out.push(...columnConstraintStatements(table, name, def))
|
|
283
|
+
if (def.comment !== undefined) out.push(commentStatement(table, name, def.comment))
|
|
284
|
+
}
|
|
285
|
+
return out
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* ALTER TYPE 시 implicit cast 가 보장되지 않아 USING 식 검토가 필요한지. 안전 목록(정수 확장·
|
|
290
|
+
* varchar↔text·decimal 정밀도·timestamp↔timestamptz·길이 변경) 밖이면 true.
|
|
291
|
+
* @param {Record<string, any>} from @param {Record<string, any>} to @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
export function isRiskyCast(from, to) {
|
|
294
|
+
const f = from.type
|
|
295
|
+
const t = to.type
|
|
296
|
+
if (f === t) return false // 같은 타입의 파라미터 변경(길이·정밀도)
|
|
297
|
+
const intFamily = ['smallInteger', 'integer', 'bigInteger', 'serial', 'bigSerial']
|
|
298
|
+
if (intFamily.includes(f) && intFamily.includes(t)) return false
|
|
299
|
+
const textFamily = ['varchar', 'text', 'char', 'enum']
|
|
300
|
+
if (textFamily.includes(f) && textFamily.includes(t)) return false
|
|
301
|
+
if ((f === 'timestamp' && t === 'timestamptz') || (f === 'timestamptz' && t === 'timestamp')) return false
|
|
302
|
+
if ((f === 'real' || f === 'doublePrecision' || f === 'decimal') && (t === 'real' || t === 'doublePrecision' || t === 'decimal')) return false
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 같은 타입의 길이/정밀도 **축소** 인지 — varchar(200→50)·decimal(12,2→8,2) 등. implicit cast 는
|
|
308
|
+
* 존재해 isRiskyCast 로는 안 잡히지만, 기존 데이터가 한도를 넘으면 적용이 실패하거나(varchar)
|
|
309
|
+
* 반올림·잘림이 생긴다 — 경고 주석 대상.
|
|
310
|
+
* @param {Record<string, any>} from @param {Record<string, any>} to @returns {boolean}
|
|
311
|
+
*/
|
|
312
|
+
export function isNarrowing(from, to) {
|
|
313
|
+
if (from.type !== to.type) return false
|
|
314
|
+
if (typeof from.length === 'number' && typeof to.length === 'number' && to.length < from.length) return true
|
|
315
|
+
if (typeof from.precision === 'number' && typeof to.precision === 'number' && to.precision < from.precision) return true
|
|
316
|
+
if (typeof from.scale === 'number' && typeof to.scale === 'number' && to.scale < from.scale) return true
|
|
317
|
+
return false
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* ALTER TYPE 문(위험 캐스트면 TODO, 같은-타입 축소면 경고 주석 동반).
|
|
322
|
+
* @param {string} table @param {string} col @param {Record<string, any>} from @param {Record<string, any>} to
|
|
323
|
+
* @returns {string}
|
|
324
|
+
*/
|
|
325
|
+
function alterTypeStatement(table, col, from, to) {
|
|
326
|
+
const target = typeSql(to, { forAlter: true })
|
|
327
|
+
let sql = `ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(col)} TYPE ${target}`
|
|
328
|
+
if (isRiskyCast(from, to)) {
|
|
329
|
+
sql = `-- TODO: implicit cast 가 없으면 적용이 실패합니다 — USING ${quoteIdent(col)}::${target} 등 캐스트 식을 검토·추가하세요.\n${sql}`
|
|
330
|
+
}
|
|
331
|
+
if (isNarrowing(from, to)) {
|
|
332
|
+
sql = `-- 경고: 길이/정밀도 축소 — 한도를 넘는 기존 데이터가 있으면 적용이 실패하거나 잘림/반올림이 발생합니다. 적용 전 데이터 분포를 확인하세요.\n${sql}`
|
|
333
|
+
}
|
|
334
|
+
return sql
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 변경 연산(op) 1개 → `{ up: string[], down: string[] }`.
|
|
339
|
+
* op 종류·필드는 {@link module:core/migration/differ} 가 정본.
|
|
340
|
+
*
|
|
341
|
+
* @param {Record<string, any>} op
|
|
342
|
+
* @returns {{ up: string[], down: string[] }}
|
|
343
|
+
*/
|
|
344
|
+
export function renderOp(op) {
|
|
345
|
+
const table = op.table
|
|
346
|
+
switch (op.kind) {
|
|
347
|
+
case 'createTable':
|
|
348
|
+
return {
|
|
349
|
+
up: createTableStatements(op.record),
|
|
350
|
+
down: [`DROP TABLE IF EXISTS ${quoteIdent(table)}`],
|
|
351
|
+
}
|
|
352
|
+
case 'dropTable': {
|
|
353
|
+
// down = 구조 복원 — 테이블·제약·코멘트(createTableStatements) + 인덱스(DROP TABLE 이 암묵
|
|
354
|
+
// 제거하므로 여기서 재생). 보유 FK 는 differ 가 별도 dropFk op 를 선행 발행하므로(의존 순서
|
|
355
|
+
// 보장) 그 op 의 down 이 복원한다 — 여기서 또 추가하면 down 에서 중복 ADD CONSTRAINT 가 된다.
|
|
356
|
+
const down = createTableStatements(op.record)
|
|
357
|
+
for (const ix of op.record.indexes ?? []) down.push(indexCreateStatement(table, ix))
|
|
358
|
+
return {
|
|
359
|
+
up: [
|
|
360
|
+
`-- 경고: 테이블과 모든 데이터가 영구 삭제됩니다 — down 으로 구조(제약·인덱스 포함)는 복원되지만 데이터는 복원되지 않습니다.\nDROP TABLE IF EXISTS ${quoteIdent(table)}`,
|
|
361
|
+
],
|
|
362
|
+
down,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
case 'addColumn': {
|
|
366
|
+
/** @type {string[]} */
|
|
367
|
+
const up = []
|
|
368
|
+
let add = `ALTER TABLE ${quoteIdent(table)} ADD COLUMN ${columnClause(op.column, op.def, { inlinePrimary: false })}`
|
|
369
|
+
if (op.def.notNull === true && op.def.default === undefined) {
|
|
370
|
+
add = `-- 경고: 기존 행이 있으면 NOT NULL 컬럼 추가가 실패합니다 — DEFAULT 를 지정하거나 backfill 후 NOT NULL 을 거세요.\n${add}`
|
|
371
|
+
}
|
|
372
|
+
up.push(add)
|
|
373
|
+
up.push(...columnConstraintStatements(table, op.column, op.def))
|
|
374
|
+
if (op.def.comment !== undefined) up.push(commentStatement(table, op.column, op.def.comment))
|
|
375
|
+
return {
|
|
376
|
+
up,
|
|
377
|
+
down: [`-- 경고: 컬럼 데이터가 영구 삭제됩니다.\nALTER TABLE ${quoteIdent(table)} DROP COLUMN ${quoteIdent(op.column)}`],
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
case 'dropColumn': {
|
|
381
|
+
// 컬럼 DROP 은 부속 제약(unique/check/fk)도 함께 제거된다(postgres 의존 객체 정리).
|
|
382
|
+
/** @type {string[]} */
|
|
383
|
+
const down = [
|
|
384
|
+
`-- 경고: 구조만 복원됩니다 — 삭제된 컬럼 데이터는 down 으로 복원되지 않습니다.\nALTER TABLE ${quoteIdent(table)} ADD COLUMN ${columnClause(op.column, op.def, { inlinePrimary: false })}`,
|
|
385
|
+
]
|
|
386
|
+
down.push(...columnConstraintStatements(table, op.column, op.def))
|
|
387
|
+
if (op.def.references !== undefined) down.push(fkAddStatement(table, op.column, op.def.references))
|
|
388
|
+
if (op.def.comment !== undefined) down.push(commentStatement(table, op.column, op.def.comment))
|
|
389
|
+
return {
|
|
390
|
+
up: [`-- 경고: 컬럼 데이터가 영구 삭제됩니다.\nALTER TABLE ${quoteIdent(table)} DROP COLUMN ${quoteIdent(op.column)}`],
|
|
391
|
+
down,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
case 'renameColumn':
|
|
395
|
+
return {
|
|
396
|
+
up: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.from)} TO ${quoteIdent(op.to)}`],
|
|
397
|
+
down: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.to)} TO ${quoteIdent(op.from)}`],
|
|
398
|
+
}
|
|
399
|
+
case 'alterType':
|
|
400
|
+
return {
|
|
401
|
+
up: [alterTypeStatement(table, op.column, op.from, op.to)],
|
|
402
|
+
down: [alterTypeStatement(table, op.column, op.to, op.from)],
|
|
403
|
+
}
|
|
404
|
+
case 'setNotNull':
|
|
405
|
+
return {
|
|
406
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET NOT NULL`],
|
|
407
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP NOT NULL`],
|
|
408
|
+
}
|
|
409
|
+
case 'dropNotNull':
|
|
410
|
+
return {
|
|
411
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP NOT NULL`],
|
|
412
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET NOT NULL`],
|
|
413
|
+
}
|
|
414
|
+
case 'setDefault':
|
|
415
|
+
return {
|
|
416
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${renderLiteral(op.value)}`],
|
|
417
|
+
down: [
|
|
418
|
+
op.prev === undefined
|
|
419
|
+
? `ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP DEFAULT`
|
|
420
|
+
: `ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${renderLiteral(op.prev)}`,
|
|
421
|
+
],
|
|
422
|
+
}
|
|
423
|
+
case 'dropDefault':
|
|
424
|
+
return {
|
|
425
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP DEFAULT`],
|
|
426
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${renderLiteral(op.prev)}`],
|
|
427
|
+
}
|
|
428
|
+
case 'addUnique':
|
|
429
|
+
return {
|
|
430
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} UNIQUE (${quoteIdent(op.column)})`],
|
|
431
|
+
down: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(op.name)}`],
|
|
432
|
+
}
|
|
433
|
+
case 'dropUnique':
|
|
434
|
+
return {
|
|
435
|
+
up: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(op.name)}`],
|
|
436
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} UNIQUE (${quoteIdent(op.column)})`],
|
|
437
|
+
}
|
|
438
|
+
// CHECK 식: enum 은 op.values 로 받아 **dialect 가 렌더**한다(식별자 인용 일원화 — differ 의
|
|
439
|
+
// 비인용 식이 camelCase 컬럼에서 적용 실패하던 결함 교정). 명시 check 는 expr 그대로.
|
|
440
|
+
case 'addCheck': {
|
|
441
|
+
const expr = op.values !== undefined ? enumCheckExpr(op.column, op.values) : op.expr
|
|
442
|
+
return {
|
|
443
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} CHECK (${expr})`],
|
|
444
|
+
down: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(op.name)}`],
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
case 'dropCheck': {
|
|
448
|
+
const expr = op.values !== undefined ? enumCheckExpr(op.column, op.values) : op.expr
|
|
449
|
+
return {
|
|
450
|
+
up: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(op.name)}`],
|
|
451
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} CHECK (${expr})`],
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
case 'addFk':
|
|
455
|
+
return {
|
|
456
|
+
up: [fkAddStatement(table, op.column, op.ref)],
|
|
457
|
+
down: [fkDropStatement(table, op.column, op.ref)],
|
|
458
|
+
}
|
|
459
|
+
case 'dropFk':
|
|
460
|
+
return {
|
|
461
|
+
up: [fkDropStatement(table, op.column, op.ref)],
|
|
462
|
+
down: [fkAddStatement(table, op.column, op.ref)],
|
|
463
|
+
}
|
|
464
|
+
// 컬럼 rename 시 FK 기본 이름(fk_<table>_<col>_<ref>) 동기화 — 실 DB 제약 이름과 명명 표준의
|
|
465
|
+
// 드리프트 방지(이후 FK 변경 마이그레이션이 옛 이름을 못 찾는 결함 교정).
|
|
466
|
+
case 'renameFk':
|
|
467
|
+
return {
|
|
468
|
+
up: [`ALTER TABLE ${quoteIdent(table)} RENAME CONSTRAINT ${quoteIdent(op.from)} TO ${quoteIdent(op.to)}`],
|
|
469
|
+
down: [`ALTER TABLE ${quoteIdent(table)} RENAME CONSTRAINT ${quoteIdent(op.to)} TO ${quoteIdent(op.from)}`],
|
|
470
|
+
}
|
|
471
|
+
// PK 제약 이름: 복합/후행 추가 = pk_<table>(우리 명명), CREATE TABLE 인라인 단일 PK = postgres
|
|
472
|
+
// 자동 명명 '<table>_pkey' — differ 가 출처를 알고 op.name 으로 확정해 넘긴다.
|
|
473
|
+
case 'addPk': {
|
|
474
|
+
const name = op.name ?? pkName(table)
|
|
475
|
+
return {
|
|
476
|
+
up: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} PRIMARY KEY (${op.columns.map(quoteIdent).join(', ')})`],
|
|
477
|
+
down: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(name)}`],
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
case 'dropPk': {
|
|
481
|
+
const name = op.name ?? pkName(table)
|
|
482
|
+
return {
|
|
483
|
+
up: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(name)}`],
|
|
484
|
+
down: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} PRIMARY KEY (${op.columns.map(quoteIdent).join(', ')})`],
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// PK 교체 — postgres 는 DROP/ADD CONSTRAINT 2문이 그대로 안전(트랜잭셔널 DDL).
|
|
488
|
+
case 'changePk': {
|
|
489
|
+
const stmts = (/** @type {any} */ from, /** @type {any} */ to) => [
|
|
490
|
+
`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT ${quoteIdent(from.name)}`,
|
|
491
|
+
`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(to.name)} PRIMARY KEY (${to.columns.map(quoteIdent).join(', ')})`,
|
|
492
|
+
]
|
|
493
|
+
return { up: stmts(op.from, op.to), down: stmts(op.to, op.from) }
|
|
494
|
+
}
|
|
495
|
+
case 'addIndex':
|
|
496
|
+
return {
|
|
497
|
+
up: [indexCreateStatement(table, op.index, { concurrent: op.concurrent === true })],
|
|
498
|
+
down: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))}`],
|
|
499
|
+
}
|
|
500
|
+
case 'dropIndex':
|
|
501
|
+
return {
|
|
502
|
+
up: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))}`],
|
|
503
|
+
down: [indexCreateStatement(table, op.index)],
|
|
504
|
+
}
|
|
505
|
+
case 'setComment':
|
|
506
|
+
return {
|
|
507
|
+
up: [commentStatement(table, op.column, op.comment)],
|
|
508
|
+
down: [commentStatement(table, op.column, op.prev ?? null)],
|
|
509
|
+
}
|
|
510
|
+
default:
|
|
511
|
+
throw new MegaConfigError('migration.schema_invalid', `postgres dialect: 알 수 없는 변경 연산 '${op.kind}'.`, { details: { kind: op.kind } })
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 연산 목록 → `{ up: string[], down: string[] }`. down 은 **역순**(가장 마지막 변경부터 되돌림).
|
|
517
|
+
* @param {Array<Record<string, any>>} ops
|
|
518
|
+
* @returns {{ up: string[], down: string[] }}
|
|
519
|
+
*/
|
|
520
|
+
export function renderOps(ops) {
|
|
521
|
+
/** @type {string[]} */
|
|
522
|
+
const up = []
|
|
523
|
+
/** @type {string[][]} */
|
|
524
|
+
const downs = []
|
|
525
|
+
for (const op of ops) {
|
|
526
|
+
const r = renderOp(op)
|
|
527
|
+
up.push(...r.up)
|
|
528
|
+
downs.push(r.down)
|
|
529
|
+
}
|
|
530
|
+
return { up, down: downs.reverse().flat() }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── DML facet (ADR-212) — DDL 마이그레이션 계약과 별개로 CRUD(model-crud)가 쓰는 DML 보조. ──
|
|
534
|
+
// 같은 모듈에 두어 driver 진실을 1곳에 유지(중복 레지스트리 신설 아님). quoteIdent 재사용.
|
|
535
|
+
|
|
536
|
+
/** placeholder 스타일 — postgres 는 번호식 `$1`. */
|
|
537
|
+
export const paramStyle = 'numbered'
|
|
538
|
+
|
|
539
|
+
/** @param {number} i - 1-base placeholder 인덱스. @returns {string} */
|
|
540
|
+
export function placeholder(i) {
|
|
541
|
+
return `$${i}`
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** RETURNING 지원(insert/upsert 레코드 반환). */
|
|
545
|
+
export const supportsReturning = true
|
|
546
|
+
/** insertMany RETURNING 지원. */
|
|
547
|
+
export const supportsBulkReturning = true
|
|
548
|
+
|
|
549
|
+
/** SELECT native 결과(pg `{ rows }`) → record[]. @param {any} res @returns {any[]} */
|
|
550
|
+
export function parseReadResult(res) {
|
|
551
|
+
return Array.isArray(res?.rows) ? res.rows : []
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** write native 결과(pg `{ rowCount, rows }`) → 정규화. @param {any} res @returns {{ count: number, rows?: any[] }} */
|
|
555
|
+
export function parseWriteResult(res) {
|
|
556
|
+
return { count: typeof res?.rowCount === 'number' ? res.rowCount : 0, rows: res?.rows }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** upsert 절 — `ON CONFLICT (...) DO UPDATE SET ...`. @param {{ conflictCols: string[], updateCols: string[] }} o @returns {string} */
|
|
560
|
+
export function upsertClause({ conflictCols, updateCols }) {
|
|
561
|
+
const set = updateCols.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(', ')
|
|
562
|
+
return `ON CONFLICT (${conflictCols.map(quoteIdent).join(', ')}) DO UPDATE SET ${set}`
|
|
563
|
+
}
|