mega-framework 0.1.5 → 0.1.7
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/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +156 -8
- package/sample/crud/.env.example +153 -28
- 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/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 +63 -3
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +478 -104
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +485 -237
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/config-validator.js +25 -0
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +223 -481
- package/src/core/mega-cluster.js +54 -13
- 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 +129 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +0 -1
- package/src/core/security.js +67 -9
- 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 +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-logger.js +1 -1
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +162 -49
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- 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/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 +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -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 +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -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 +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -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 +106 -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 +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -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 +80 -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 +264 -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 +92 -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 +18 -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 +96 -0
- package/types/core/ws-upgrade.d.ts +231 -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 +241 -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 +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -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 +127 -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 +234 -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,476 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* SQLite dialect (ADR-207) — `dialects/README.md` contract 구현.
|
|
4
|
+
*
|
|
5
|
+
* postgres 와의 핵심 차이:
|
|
6
|
+
* - **type affinity** — varchar/char/uuid/timestamp 류는 전부 TEXT, 정수 계열은 INTEGER(64bit),
|
|
7
|
+
* serial 류는 인라인 PK 일 때만 `INTEGER PRIMARY KEY AUTOINCREMENT`.
|
|
8
|
+
* - **`dependsOnRebuild: true`** — 컬럼 drop/타입 변경/제약 변경 등은 sqlite ALTER 가 표현하지
|
|
9
|
+
* 못해 differ 가 그 테이블의 모든 변경을 `rebuildTable` 1개로 수렴하고, 본 dialect 가
|
|
10
|
+
* 공식 12-step 재생성 절차를 렌더한다(데이터는 컬럼 매핑 INSERT…SELECT 로 보존).
|
|
11
|
+
* 재생성 대상 테이블의 트리거/뷰는 generate 가 DB 파일을 직접 읽어 검사한다
|
|
12
|
+
* (`inspectRebuildHazards`) — 뷰 충돌은 fail-fast, 트리거는 파일 헤더·콘솔 경고로 이름 명시
|
|
13
|
+
* (재생성 SQL 은 사용자가 추가).
|
|
14
|
+
* - **FK 는 인라인 정의**(`supportsAlterAddFk: false`) — sqlite 는 ALTER 로 FK 를 추가/삭제할 수
|
|
15
|
+
* 없다. CREATE TABLE 에 named 테이블 제약으로 싣고(스키마는 forward reference 허용), FK 변경은
|
|
16
|
+
* rebuild 경로. RENAME COLUMN 은 스키마 내 참조를 자동 갱신하므로 FK 이름 동기화 자체가 불필요.
|
|
17
|
+
* - UNIQUE 는 제약이 아니라 **UNIQUE INDEX** 로 렌더 — 후행 추가/삭제가 rebuild 없이 가능하고
|
|
18
|
+
* 명명 표준(uniq_*)이 그대로 diff 식별자가 된다.
|
|
19
|
+
* - 컬럼 코멘트 미지원 — setComment 는 SQL 주석 한 줄로 대체(명시 안내, silent 누락 아님).
|
|
20
|
+
* - `PRAGMA foreign_keys` 는 트랜잭션 안에서 변경 불가 — rebuild 마이그레이션은
|
|
21
|
+
* `export const transaction = false`(generate 가 자동 설정) + 내부 BEGIN/COMMIT 으로 진행한다.
|
|
22
|
+
*
|
|
23
|
+
* @module core/migration/dialects/sqlite
|
|
24
|
+
*/
|
|
25
|
+
import { MegaConfigError } from '../../../errors/config-error.js'
|
|
26
|
+
|
|
27
|
+
// ── dialect 표준 인터페이스 속성 ─────────────────────────────────────────────
|
|
28
|
+
/** sqlite 는 식별자 길이 제한이 사실상 없다. */
|
|
29
|
+
export const identifierMaxBytes = Infinity
|
|
30
|
+
/** CONCURRENTLY 동등물 없음 — --concurrent 는 generate 가 명시 거부. */
|
|
31
|
+
export const supportsConcurrentIndex = false
|
|
32
|
+
/** enum 표현 전략 — TEXT + CHECK(col IN (...)). */
|
|
33
|
+
export const enumStrategy = 'check'
|
|
34
|
+
/** RENAME COLUMN(3.25+)은 트랜잭션 내 정상 동작. */
|
|
35
|
+
export const supportsRenameInTx = true
|
|
36
|
+
/** 단순 DROP COLUMN(3.35+)도 트랜잭션 대상 — 단 본 dialect 는 호환 위해 rebuild 로 처리. */
|
|
37
|
+
export const canDropColumnInTx = true
|
|
38
|
+
/** 핵심 — 다수 ALTER 미지원이라 테이블 재생성(12-step) 패턴 사용. */
|
|
39
|
+
export const dependsOnRebuild = true
|
|
40
|
+
/** RENAME CONSTRAINT 없음. (FK 는 인라인이라 이름 동기화 자체가 불필요.) */
|
|
41
|
+
export const supportsRenameConstraint = false
|
|
42
|
+
/** ALTER 로 FK 추가/삭제 불가 — CREATE TABLE 인라인 + 변경은 rebuild. */
|
|
43
|
+
export const supportsAlterAddFk = false
|
|
44
|
+
/** 컬럼/FK 변경은 rebuild 로 수렴 — dropFk 선행 발행 자체가 없다. */
|
|
45
|
+
export const requiresDropFkBeforeDropColumn = false
|
|
46
|
+
/** SQL DDL 렌더 dialect — 생성 파일이 `db.query(sql)` 계약을 쓴다(mongo 와 분기, ADR-209). */
|
|
47
|
+
export const usesSqlDdl = true
|
|
48
|
+
|
|
49
|
+
/** bare 허용 식별자 — 그 외는 따옴표. */
|
|
50
|
+
const BARE_IDENT_RE = /^[a-z_][a-z0-9_]*$/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* SQL 식별자 인용 — postgres 와 동일한 더블쿼트(필요 시만).
|
|
54
|
+
* @param {string} name @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function quoteIdent(name) {
|
|
57
|
+
if (BARE_IDENT_RE.test(name)) return name
|
|
58
|
+
return `"${name.replaceAll('"', '""')}"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 값 literal 인용 — '' 이스케이프(sqlite 는 백슬래시 escape 없음). boolean 은 INTEGER 0/1.
|
|
63
|
+
* @param {unknown} value @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function quoteLiteral(value) {
|
|
66
|
+
if (value === null) return 'NULL'
|
|
67
|
+
if (typeof value === 'number') {
|
|
68
|
+
if (!Number.isFinite(value)) {
|
|
69
|
+
throw new MegaConfigError('migration.schema_invalid', `default 값이 유한한 수가 아닙니다: ${value}`, { details: { value: String(value) } })
|
|
70
|
+
}
|
|
71
|
+
return String(value)
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'boolean') return value ? '1' : '0' // boolean → INTEGER affinity 정합
|
|
74
|
+
if (typeof value === 'object' && value !== null && typeof (/** @type {any} */ (value).raw) === 'string') {
|
|
75
|
+
return /** @type {any} */ (value).raw
|
|
76
|
+
}
|
|
77
|
+
return `'${String(value).replaceAll("'", "''")}'`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 컬럼 타입 → sqlite 타입(affinity).
|
|
82
|
+
* @param {Record<string, any>} def @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function typeSql(def) {
|
|
85
|
+
switch (def.type) {
|
|
86
|
+
case 'serial':
|
|
87
|
+
case 'bigSerial':
|
|
88
|
+
case 'integer':
|
|
89
|
+
case 'bigInteger':
|
|
90
|
+
case 'smallInteger':
|
|
91
|
+
case 'boolean': // 0/1 — INTEGER affinity
|
|
92
|
+
return 'INTEGER'
|
|
93
|
+
case 'real':
|
|
94
|
+
case 'doublePrecision':
|
|
95
|
+
return 'REAL'
|
|
96
|
+
case 'decimal':
|
|
97
|
+
return `NUMERIC(${def.precision}, ${def.scale})`
|
|
98
|
+
case 'varchar':
|
|
99
|
+
case 'text':
|
|
100
|
+
case 'char':
|
|
101
|
+
case 'uuid':
|
|
102
|
+
case 'timestamp':
|
|
103
|
+
case 'timestamptz': // ISO8601 TEXT 저장 규약(ADR-207)
|
|
104
|
+
case 'date':
|
|
105
|
+
case 'time':
|
|
106
|
+
case 'json':
|
|
107
|
+
case 'jsonb':
|
|
108
|
+
case 'enum':
|
|
109
|
+
return 'TEXT'
|
|
110
|
+
case 'bytea':
|
|
111
|
+
return 'BLOB'
|
|
112
|
+
default:
|
|
113
|
+
throw new MegaConfigError('migration.schema_invalid', `sqlite dialect: 알 수 없는 타입 '${def.type}'.`, { details: { type: def.type } })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 명명 표준 — postgres 와 동일 규칙 ────────────────────────────────────────
|
|
118
|
+
/** @param {string} table @param {string[]} cols @param {boolean} [unique] @returns {string} */
|
|
119
|
+
export function indexName(table, cols, unique = false) {
|
|
120
|
+
return `${unique ? 'uniq' : 'idx'}_${table}_${cols.join('_')}`
|
|
121
|
+
}
|
|
122
|
+
/** @param {string} table @param {Record<string, any>} ix @returns {string} */
|
|
123
|
+
export function resolveIndexName(table, ix) {
|
|
124
|
+
if (ix.name !== undefined) return ix.name
|
|
125
|
+
if (ix.expression !== undefined) {
|
|
126
|
+
throw new MegaConfigError('migration.schema_invalid', `표현식 인덱스는 name 이 필수입니다 (table '${table}').`, { details: { table } })
|
|
127
|
+
}
|
|
128
|
+
return indexName(table, ix.columns, ix.unique === true)
|
|
129
|
+
}
|
|
130
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
131
|
+
export function uniqueName(table, col) {
|
|
132
|
+
return `uniq_${table}_${col}`
|
|
133
|
+
}
|
|
134
|
+
/** @param {string} table @param {string} col @param {string} refTable @returns {string} */
|
|
135
|
+
export function fkName(table, col, refTable) {
|
|
136
|
+
return `fk_${table}_${col}_${refTable}`
|
|
137
|
+
}
|
|
138
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
139
|
+
export function checkName(table, col) {
|
|
140
|
+
return `chk_${table}_${col}`
|
|
141
|
+
}
|
|
142
|
+
/** @param {string} table @returns {string} */
|
|
143
|
+
export function pkName(table) {
|
|
144
|
+
return `pk_${table}`
|
|
145
|
+
}
|
|
146
|
+
/** sqlite 인라인 PK 는 별도 제약 이름이 없다 — diff 정규화용 합성 명칭. @param {string} table @returns {string} */
|
|
147
|
+
export function inlinePkName(table) {
|
|
148
|
+
return `pk_${table}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* enum 의 CHECK 식 — 유일한 enum 렌더 지점(differ 는 values 만 전달).
|
|
153
|
+
* @param {string} col @param {string[]} values @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
export function enumCheckExpr(col, values) {
|
|
156
|
+
return `${quoteIdent(col)} IN (${values.map((v) => quoteLiteral(v)).join(', ')})`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 컬럼 정의 절. serial 류 + 인라인 PK 는 `INTEGER PRIMARY KEY AUTOINCREMENT`(rowid alias).
|
|
161
|
+
* enum/명시 CHECK 는 컬럼 절이 아니라 테이블 제약(named)으로 — 호출부가 별도 합성.
|
|
162
|
+
* @param {string} name @param {Record<string, any>} def @param {{ inlinePrimary?: boolean }} [opts]
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
function columnClause(name, def, { inlinePrimary = true } = {}) {
|
|
166
|
+
const isSerial = def.type === 'serial' || def.type === 'bigSerial'
|
|
167
|
+
let sql = `${quoteIdent(name)} ${typeSql(def)}`
|
|
168
|
+
if (inlinePrimary && def.primary === true) {
|
|
169
|
+
sql += isSerial ? ' PRIMARY KEY AUTOINCREMENT' : ' PRIMARY KEY'
|
|
170
|
+
}
|
|
171
|
+
if (def.notNull === true) sql += ' NOT NULL'
|
|
172
|
+
if (def.default !== undefined) sql += ` DEFAULT ${quoteLiteral(def.default)}`
|
|
173
|
+
return sql
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 컬럼의 CHECK 테이블 제약 절(enum + 명시 check) — CREATE TABLE 본문용.
|
|
178
|
+
* @param {string} table @param {string} col @param {Record<string, any>} def @returns {string[]}
|
|
179
|
+
*/
|
|
180
|
+
function checkConstraintClauses(table, col, def) {
|
|
181
|
+
/** @type {string[]} */
|
|
182
|
+
const out = []
|
|
183
|
+
if (def.type === 'enum') {
|
|
184
|
+
out.push(`CONSTRAINT ${quoteIdent(def.enumName ?? checkName(table, col))} CHECK (${enumCheckExpr(col, def.values)})`)
|
|
185
|
+
}
|
|
186
|
+
if (def.check !== undefined) {
|
|
187
|
+
out.push(`CONSTRAINT ${quoteIdent(def.check.name ?? checkName(table, col))} CHECK (${def.check.expr})`)
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** FK 테이블 제약 절. @param {string} table @param {string} col @param {Record<string, any>} ref @returns {string} */
|
|
193
|
+
function fkConstraintClause(table, col, ref) {
|
|
194
|
+
const name = ref.name ?? fkName(table, col, ref.table)
|
|
195
|
+
let sql = `CONSTRAINT ${quoteIdent(name)} FOREIGN KEY (${quoteIdent(col)}) REFERENCES ${quoteIdent(ref.table)} (${quoteIdent(ref.column)})`
|
|
196
|
+
if (ref.onDelete !== undefined) sql += ` ON DELETE ${ref.onDelete.toUpperCase()}`
|
|
197
|
+
if (ref.onUpdate !== undefined) sql += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`
|
|
198
|
+
return sql
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* UNIQUE 인덱스 생성 문 — sqlite 는 UNIQUE 를 제약이 아니라 인덱스로 렌더(후행 추가/삭제가
|
|
203
|
+
* rebuild 없이 가능, 명명 표준 유지).
|
|
204
|
+
* @param {string} table @param {string} col @param {Record<string, any>} def @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
function uniqueIndexStatements(table, col, def) {
|
|
207
|
+
if (def.unique === undefined) return []
|
|
208
|
+
const name = def.unique === true ? uniqueName(table, col) : def.unique.name
|
|
209
|
+
return [`CREATE UNIQUE INDEX ${quoteIdent(name)} ON ${quoteIdent(table)} (${quoteIdent(col)})`]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 인덱스 생성 문 — 부분(WHERE)·표현식 인덱스 지원(sqlite ✓). USING 은 미지원(거부).
|
|
214
|
+
* @param {string} table @param {Record<string, any>} ix @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
function indexCreateStatement(table, ix) {
|
|
217
|
+
if (ix.using !== undefined) {
|
|
218
|
+
throw new MegaConfigError('migration.dialect_feature_unsupported', `sqlite 는 인덱스 USING 절을 지원하지 않습니다 (table '${table}').`, {
|
|
219
|
+
details: { table, using: ix.using },
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
const name = resolveIndexName(table, ix)
|
|
223
|
+
const target = ix.expression !== undefined ? `(${ix.expression})` : `(${ix.columns.map(quoteIdent).join(', ')})`
|
|
224
|
+
let sql = `CREATE ${ix.unique === true ? 'UNIQUE ' : ''}INDEX ${quoteIdent(name)} ON ${quoteIdent(table)} ${target}`
|
|
225
|
+
if (ix.where !== undefined) sql += ` WHERE ${ix.where}`
|
|
226
|
+
return sql
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* CREATE TABLE 문(+UNIQUE 인덱스) — FK·CHECK 는 named 테이블 제약으로 본문 인라인.
|
|
231
|
+
* @param {Record<string, any>} record @param {{ tableNameOverride?: string }} [opts] - rebuild 의 임시 테이블명.
|
|
232
|
+
* @returns {string[]}
|
|
233
|
+
*/
|
|
234
|
+
function createTableStatements(record, { tableNameOverride } = {}) {
|
|
235
|
+
const table = tableNameOverride ?? record.table
|
|
236
|
+
/** @type {string[]} */
|
|
237
|
+
const lines = []
|
|
238
|
+
for (const [name, def] of Object.entries(record.columns)) {
|
|
239
|
+
lines.push(' ' + columnClause(name, def))
|
|
240
|
+
}
|
|
241
|
+
if (record.primaryKey !== undefined) {
|
|
242
|
+
lines.push(` CONSTRAINT ${quoteIdent(pkName(record.table))} PRIMARY KEY (${record.primaryKey.map(quoteIdent).join(', ')})`)
|
|
243
|
+
}
|
|
244
|
+
for (const [name, def] of Object.entries(record.columns)) {
|
|
245
|
+
for (const c of checkConstraintClauses(record.table, name, def)) lines.push(' ' + c)
|
|
246
|
+
if (def.references !== undefined) lines.push(' ' + fkConstraintClause(record.table, name, def.references))
|
|
247
|
+
}
|
|
248
|
+
/** @type {string[]} */
|
|
249
|
+
const out = [`CREATE TABLE ${quoteIdent(table)} (\n${lines.join(',\n')}\n)`]
|
|
250
|
+
if (tableNameOverride === undefined) {
|
|
251
|
+
for (const [name, def] of Object.entries(record.columns)) out.push(...uniqueIndexStatements(record.table, name, def))
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 12-step 테이블 재생성 — 공식 절차(PRAGMA OFF → tx → 새 테이블 → 데이터 이주 → 교체 → 인덱스 →
|
|
258
|
+
* fk check → COMMIT → PRAGMA ON). 트리거/뷰 재생성은 범위 밖(모듈 docstring·생성 파일 헤더 명시).
|
|
259
|
+
* @param {Record<string, any>} from @param {Record<string, any>} to
|
|
260
|
+
* @param {Record<string, string>} renames - from→to 컬럼 매핑.
|
|
261
|
+
* @returns {string[]}
|
|
262
|
+
*/
|
|
263
|
+
function rebuildStatements(from, to, renames) {
|
|
264
|
+
const table = to.table
|
|
265
|
+
const tmp = `_new_${table}`
|
|
266
|
+
// BEGIN IMMEDIATE — 시작 시점에 쓰기 락을 선점해 동시 러너의 rebuild 가 절차 중간에 끼어들지
|
|
267
|
+
// 못하게 한다(deferred BEGIN 은 첫 쓰기까지 락이 없어 read-check-apply 경합 창이 넓다).
|
|
268
|
+
/** @type {string[]} */
|
|
269
|
+
const out = ['PRAGMA foreign_keys=OFF', 'BEGIN IMMEDIATE']
|
|
270
|
+
out.push(...createTableStatements(to, { tableNameOverride: tmp }))
|
|
271
|
+
// 데이터 이주 — to 컬럼별 출처: rename 매핑 → 동명 컬럼 → (없으면 제외 = DEFAULT/NULL).
|
|
272
|
+
/** @type {string[]} */
|
|
273
|
+
const targetCols = []
|
|
274
|
+
/** @type {string[]} */
|
|
275
|
+
const sourceCols = []
|
|
276
|
+
for (const colName of Object.keys(to.columns)) {
|
|
277
|
+
const renamedFrom = Object.entries(renames).find(([, n]) => n === colName)?.[0]
|
|
278
|
+
const source = renamedFrom ?? (from.columns[colName] !== undefined ? colName : undefined)
|
|
279
|
+
if (source !== undefined) {
|
|
280
|
+
targetCols.push(quoteIdent(colName))
|
|
281
|
+
sourceCols.push(quoteIdent(source))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (targetCols.length > 0) {
|
|
285
|
+
out.push(`INSERT INTO ${quoteIdent(tmp)} (${targetCols.join(', ')}) SELECT ${sourceCols.join(', ')} FROM ${quoteIdent(table)}`)
|
|
286
|
+
}
|
|
287
|
+
out.push(`DROP TABLE ${quoteIdent(table)}`)
|
|
288
|
+
out.push(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(table)}`)
|
|
289
|
+
for (const [name, def] of Object.entries(to.columns)) out.push(...uniqueIndexStatements(table, name, def))
|
|
290
|
+
for (const ix of to.indexes ?? []) out.push(indexCreateStatement(table, ix))
|
|
291
|
+
out.push('PRAGMA foreign_key_check') // 위반 행을 반환(throw 아님) — 결과 검토는 운영자 몫(헤더 안내)
|
|
292
|
+
out.push('COMMIT')
|
|
293
|
+
out.push('PRAGMA foreign_keys=ON')
|
|
294
|
+
return out
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** renames(from→to)를 to→from 으로 뒤집는다. @param {Record<string, string>} renames @returns {Record<string, string>} */
|
|
298
|
+
function invertRenames(renames) {
|
|
299
|
+
return Object.fromEntries(Object.entries(renames).map(([a, b]) => [b, a]))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* rebuild 위험 요소 검사(best-effort) — 대상 DB 파일이 있으면 읽기 전용으로 열어 sqlite_master 에서
|
|
304
|
+
* 재생성 테이블의 **트리거**(DROP TABLE 과 함께 소실 — 사용자 재생성 필요)와 테이블을 참조하는
|
|
305
|
+
* **뷰**(3.25+ 가 RENAME 시점에 뷰를 재파싱해 rebuild 가 통째 실패)를 찾는다. generate 는 원래
|
|
306
|
+
* DB 무연결 설계지만 sqlite 는 "서버" 가 아니라 파일이라 이 검사가 유일한 사전 방어선이다.
|
|
307
|
+
* 파일 부재(첫 generate 등)·열기 실패는 빈 결과 — 헤더의 일반 경고가 잔여 방어선.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} filename - sqlite DB 파일 경로(':memory:' 면 검사 불가).
|
|
310
|
+
* @param {string[]} tables - rebuild 대상 테이블들.
|
|
311
|
+
* @returns {Promise<{ triggers: string[], conflictViews: string[] }>}
|
|
312
|
+
*/
|
|
313
|
+
export async function inspectRebuildHazards(filename, tables) {
|
|
314
|
+
/** @type {{ triggers: string[], conflictViews: string[] }} */
|
|
315
|
+
const empty = { triggers: [], conflictViews: [] }
|
|
316
|
+
if (typeof filename !== 'string' || filename === ':memory:') return empty
|
|
317
|
+
let db
|
|
318
|
+
try {
|
|
319
|
+
const { default: Database } = await import('better-sqlite3')
|
|
320
|
+
db = new Database(filename, { readonly: true, fileMustExist: true })
|
|
321
|
+
} catch {
|
|
322
|
+
// 파일 부재/열기 실패 — 첫 generate(아직 DB 없음) 등 정상 경로. 헤더 일반 경고로 폴백.
|
|
323
|
+
return empty
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const rows = /** @type {Array<{ type: string, name: string, tbl_name: string, sql: string | null }>} */ (
|
|
327
|
+
db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master WHERE type IN ('trigger', 'view')").all()
|
|
328
|
+
)
|
|
329
|
+
const triggers = rows.filter((r) => r.type === 'trigger' && tables.includes(r.tbl_name)).map((r) => r.name)
|
|
330
|
+
// 뷰는 SQL 본문이 대상 테이블을 단어 경계로 언급하면 충돌 후보(보수적 — 과탐지는 안전 방향).
|
|
331
|
+
const conflictViews = rows
|
|
332
|
+
.filter((r) => r.type === 'view' && typeof r.sql === 'string' && tables.some((t) => new RegExp(`\\b${t}\\b`, 'i').test(/** @type {string} */ (r.sql))))
|
|
333
|
+
.map((r) => r.name)
|
|
334
|
+
return { triggers, conflictViews }
|
|
335
|
+
} finally {
|
|
336
|
+
db.close()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 변경 연산(op) 1개 → `{ up: string[], down: string[] }`.
|
|
342
|
+
* setNotNull/setDefault/addCheck/addFk/… 류는 differ 가 rebuildTable 로 수렴하므로 도달하지 않는다
|
|
343
|
+
* (도달 시 명시 에러 — silent 누락 금지).
|
|
344
|
+
* @param {Record<string, any>} op
|
|
345
|
+
* @returns {{ up: string[], down: string[] }}
|
|
346
|
+
*/
|
|
347
|
+
export function renderOp(op) {
|
|
348
|
+
const table = op.table
|
|
349
|
+
switch (op.kind) {
|
|
350
|
+
case 'createTable':
|
|
351
|
+
return { up: createTableStatements(op.record), down: [`DROP TABLE IF EXISTS ${quoteIdent(table)}`] }
|
|
352
|
+
case 'dropTable': {
|
|
353
|
+
const down = createTableStatements(op.record)
|
|
354
|
+
for (const ix of op.record.indexes ?? []) down.push(indexCreateStatement(table, ix))
|
|
355
|
+
return {
|
|
356
|
+
up: [
|
|
357
|
+
// FK 로 얽힌 테이블 동시 제거 — sqlite 는 dropFk 가 불가하므로 트랜잭션 한정 지연 검사로 순서 무관화.
|
|
358
|
+
'PRAGMA defer_foreign_keys=ON',
|
|
359
|
+
`-- 경고: 테이블과 모든 데이터가 영구 삭제됩니다 — down 으로 구조(제약·인덱스 포함)는 복원되지만 데이터는 복원되지 않습니다.\nDROP TABLE IF EXISTS ${quoteIdent(table)}`,
|
|
360
|
+
],
|
|
361
|
+
down,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
case 'addColumn': {
|
|
365
|
+
if (op.def.primary === true) {
|
|
366
|
+
throw new MegaConfigError('migration.unsupported_change', `sqlite: 기존 테이블에 PRIMARY KEY 컬럼 추가는 재생성이 필요합니다 — 다른 변경과 함께 적용하거나 raw 마이그레이션으로 작성하세요 (table '${table}').`, {
|
|
367
|
+
details: { table, column: op.column },
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
/** @type {string[]} */
|
|
371
|
+
const up = []
|
|
372
|
+
// ADD COLUMN 은 컬럼 제약만 허용 — named CHECK·인라인 REFERENCES 를 컬럼 절에 직렬로 붙인다.
|
|
373
|
+
let clause = columnClause(op.column, op.def, { inlinePrimary: false })
|
|
374
|
+
for (const c of checkConstraintClauses(table, op.column, op.def)) clause += ` ${c}`
|
|
375
|
+
const ref = op.def.references
|
|
376
|
+
if (ref !== undefined) {
|
|
377
|
+
// 인라인 REFERENCES — sqlite 허용(컬럼이 NULL 허용일 때). NOT NULL FK 추가는 rebuild 경로.
|
|
378
|
+
clause += ` REFERENCES ${quoteIdent(ref.table)} (${quoteIdent(ref.column)})`
|
|
379
|
+
if (ref.onDelete !== undefined) clause += ` ON DELETE ${ref.onDelete.toUpperCase()}`
|
|
380
|
+
if (ref.onUpdate !== undefined) clause += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`
|
|
381
|
+
}
|
|
382
|
+
let add = `ALTER TABLE ${quoteIdent(table)} ADD COLUMN ${clause}`
|
|
383
|
+
if (op.def.notNull === true && op.def.default === undefined) {
|
|
384
|
+
add = `-- 경고: sqlite 는 기존 행이 있으면 DEFAULT 없는 NOT NULL 컬럼 추가가 실패합니다.\n${add}`
|
|
385
|
+
}
|
|
386
|
+
up.push(add)
|
|
387
|
+
up.push(...uniqueIndexStatements(table, op.column, op.def))
|
|
388
|
+
return {
|
|
389
|
+
up,
|
|
390
|
+
down: [`-- 경고: 컬럼 데이터가 영구 삭제됩니다.\nALTER TABLE ${quoteIdent(table)} DROP COLUMN ${quoteIdent(op.column)}`],
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
case 'renameColumn':
|
|
394
|
+
return {
|
|
395
|
+
up: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.from)} TO ${quoteIdent(op.to)}`],
|
|
396
|
+
down: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.to)} TO ${quoteIdent(op.from)}`],
|
|
397
|
+
}
|
|
398
|
+
case 'addUnique':
|
|
399
|
+
return {
|
|
400
|
+
up: [`CREATE UNIQUE INDEX ${quoteIdent(op.name)} ON ${quoteIdent(table)} (${quoteIdent(op.column)})`],
|
|
401
|
+
down: [`DROP INDEX IF EXISTS ${quoteIdent(op.name)}`],
|
|
402
|
+
}
|
|
403
|
+
case 'dropUnique':
|
|
404
|
+
return {
|
|
405
|
+
up: [`DROP INDEX IF EXISTS ${quoteIdent(op.name)}`],
|
|
406
|
+
down: [`CREATE UNIQUE INDEX ${quoteIdent(op.name)} ON ${quoteIdent(table)} (${quoteIdent(op.column)})`],
|
|
407
|
+
}
|
|
408
|
+
case 'addIndex':
|
|
409
|
+
return {
|
|
410
|
+
up: [indexCreateStatement(table, op.index)],
|
|
411
|
+
down: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))}`],
|
|
412
|
+
}
|
|
413
|
+
case 'dropIndex':
|
|
414
|
+
return {
|
|
415
|
+
up: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))}`],
|
|
416
|
+
down: [indexCreateStatement(table, op.index)],
|
|
417
|
+
}
|
|
418
|
+
case 'setComment':
|
|
419
|
+
// sqlite 는 컬럼 코멘트가 없다 — 누락을 숨기지 않고 SQL 주석으로 명시(실행 무해).
|
|
420
|
+
return {
|
|
421
|
+
up: [`-- sqlite: 컬럼 코멘트 미지원 — '${table}.${op.column}' 의 코멘트 변경은 적용되지 않습니다.`],
|
|
422
|
+
down: [`-- sqlite: 컬럼 코멘트 미지원 — '${table}.${op.column}' 의 코멘트 변경은 적용되지 않습니다.`],
|
|
423
|
+
}
|
|
424
|
+
case 'rebuildTable':
|
|
425
|
+
return {
|
|
426
|
+
up: rebuildStatements(op.from, op.to, op.renames ?? {}),
|
|
427
|
+
down: rebuildStatements(op.to, op.from, invertRenames(op.renames ?? {})),
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
// setNotNull/alterType/addFk 류는 differ 가 rebuildTable 로 수렴 — 도달은 differ 결함.
|
|
431
|
+
throw new MegaConfigError('migration.schema_invalid', `sqlite dialect: '${op.kind}' 는 직접 렌더 대상이 아닙니다(rebuildTable 로 수렴돼야 함).`, {
|
|
432
|
+
details: { kind: op.kind },
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 연산 목록 → `{ up, down }` (down 은 역순).
|
|
439
|
+
* @param {Array<Record<string, any>>} ops
|
|
440
|
+
* @returns {{ up: string[], down: string[] }}
|
|
441
|
+
*/
|
|
442
|
+
export function renderOps(ops) {
|
|
443
|
+
/** @type {string[]} */
|
|
444
|
+
const up = []
|
|
445
|
+
/** @type {string[][]} */
|
|
446
|
+
const downs = []
|
|
447
|
+
for (const op of ops) {
|
|
448
|
+
const r = renderOp(op)
|
|
449
|
+
up.push(...r.up)
|
|
450
|
+
downs.push(r.down)
|
|
451
|
+
}
|
|
452
|
+
return { up, down: downs.reverse().flat() }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── DML facet (ADR-212) — CRUD(model-crud)용 DML 보조. better-sqlite3 RETURNING 지원(실측). ──
|
|
456
|
+
export const paramStyle = 'positional'
|
|
457
|
+
/** sqlite 는 위치식 `?`(인덱스 무시). @returns {string} */
|
|
458
|
+
export function placeholder() {
|
|
459
|
+
return '?'
|
|
460
|
+
}
|
|
461
|
+
export const supportsReturning = true
|
|
462
|
+
export const supportsBulkReturning = true
|
|
463
|
+
/** SELECT native(stmt.all 배열) → record[]. @param {any} res @returns {any[]} */
|
|
464
|
+
export function parseReadResult(res) {
|
|
465
|
+
return Array.isArray(res) ? res : []
|
|
466
|
+
}
|
|
467
|
+
/** write native → 정규화. RETURNING 이면 stmt.all 배열, 아니면 `{ changes, lastInsertRowid }`. @param {any} res */
|
|
468
|
+
export function parseWriteResult(res) {
|
|
469
|
+
if (Array.isArray(res)) return { count: res.length, rows: res }
|
|
470
|
+
return { count: Number(res?.changes ?? 0), lastInsertRowid: res?.lastInsertRowid }
|
|
471
|
+
}
|
|
472
|
+
/** upsert 절 — `ON CONFLICT (...) DO UPDATE SET col = excluded.col`. @param {{ conflictCols: string[], updateCols: string[] }} o */
|
|
473
|
+
export function upsertClause({ conflictCols, updateCols }) {
|
|
474
|
+
const set = updateCols.map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`).join(', ')
|
|
475
|
+
return `ON CONFLICT (${conflictCols.map(quoteIdent).join(', ')}) DO UPDATE SET ${set}`
|
|
476
|
+
}
|