@simplysm/orm-common 13.0.100 → 14.0.4
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 +90 -147
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +312 -276
- package/dist/create-db-context.js.map +1 -6
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/column-ddl.js +41 -35
- package/dist/ddl/column-ddl.js.map +1 -6
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +200 -142
- package/dist/ddl/initialize.js.map +1 -6
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/relation-ddl.js +55 -48
- package/dist/ddl/relation-ddl.js.map +1 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/schema-ddl.js +21 -15
- package/dist/ddl/schema-ddl.js.map +1 -6
- package/dist/ddl/table-ddl.d.ts +20 -20
- package/dist/ddl/table-ddl.js +139 -93
- package/dist/ddl/table-ddl.js.map +1 -6
- package/dist/define-db-context.js +10 -13
- package/dist/define-db-context.js.map +1 -6
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/errors/db-transaction-error.js +53 -19
- package/dist/errors/db-transaction-error.js.map +1 -6
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +94 -40
- package/dist/exec/executable.js.map +1 -6
- package/dist/exec/queryable.d.ts +97 -97
- package/dist/exec/queryable.js +1310 -1204
- package/dist/exec/queryable.js.map +1 -6
- package/dist/exec/search-parser.d.ts +31 -31
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/exec/search-parser.js +158 -59
- package/dist/exec/search-parser.js.map +1 -6
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr-unit.js +24 -18
- package/dist/expr/expr-unit.js.map +1 -6
- package/dist/expr/expr.d.ts +108 -108
- package/dist/expr/expr.js +1872 -1844
- package/dist/expr/expr.js.map +1 -6
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -6
- package/dist/models/system-migration.js +7 -7
- package/dist/models/system-migration.js.map +1 -6
- package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
- package/dist/query-builder/base/expr-renderer-base.js +27 -21
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
- package/dist/query-builder/base/query-builder-base.d.ts +21 -21
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +90 -80
- package/dist/query-builder/base/query-builder-base.js.map +1 -6
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/query-builder/query-builder.js +13 -13
- package/dist/query-builder/query-builder.js.map +1 -6
- package/dist/schema/factory/column-builder.d.ts +84 -84
- package/dist/schema/factory/column-builder.js +248 -185
- package/dist/schema/factory/column-builder.js.map +1 -6
- package/dist/schema/factory/index-builder.d.ts +38 -38
- package/dist/schema/factory/index-builder.js +144 -85
- package/dist/schema/factory/index-builder.js.map +1 -6
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +274 -136
- package/dist/schema/factory/relation-builder.js.map +1 -6
- package/dist/schema/procedure-builder.d.ts +51 -51
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +205 -131
- package/dist/schema/procedure-builder.js.map +1 -6
- package/dist/schema/table-builder.d.ts +55 -55
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +274 -205
- package/dist/schema/table-builder.js.map +1 -6
- package/dist/schema/view-builder.d.ts +44 -44
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +189 -116
- package/dist/schema/view-builder.js.map +1 -6
- package/dist/types/column.d.ts +21 -21
- package/dist/types/column.js +60 -30
- package/dist/types/column.js.map +1 -6
- package/dist/types/db-context-def.d.ts +9 -9
- package/dist/types/db-context-def.js +2 -1
- package/dist/types/db-context-def.js.map +1 -6
- package/dist/types/db.d.ts +47 -47
- package/dist/types/db.js +15 -5
- package/dist/types/db.js.map +1 -6
- package/dist/types/expr.d.ts +81 -81
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/expr.js +3 -1
- package/dist/types/expr.js.map +1 -6
- package/dist/types/query-def.d.ts +46 -46
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/types/query-def.js +31 -24
- package/dist/types/query-def.js.map +1 -6
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/docs/core.md +117 -145
- package/docs/expression.md +186 -203
- package/docs/query-builder.md +75 -42
- package/docs/queryable.md +189 -151
- package/docs/schema-builders.md +172 -283
- package/docs/types.md +229 -173
- package/package.json +7 -5
- package/src/create-db-context.ts +31 -31
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +38 -38
- package/src/ddl/relation-ddl.ts +6 -6
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +24 -24
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +152 -152
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +118 -118
- package/src/index.ts +8 -8
- package/src/models/system-migration.ts +1 -1
- package/src/query-builder/base/expr-renderer-base.ts +21 -21
- package/src/query-builder/base/query-builder-base.ts +33 -33
- package/src/query-builder/mssql/mssql-expr-renderer.ts +28 -28
- package/src/query-builder/mssql/mssql-query-builder.ts +37 -37
- package/src/query-builder/mysql/mysql-expr-renderer.ts +29 -29
- package/src/query-builder/mysql/mysql-query-builder.ts +70 -70
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +22 -22
- package/src/query-builder/postgresql/postgresql-query-builder.ts +54 -54
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +86 -86
- package/src/schema/factory/index-builder.ts +38 -38
- package/src/schema/factory/relation-builder.ts +102 -102
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +47 -47
- package/src/types/column.ts +24 -24
- package/src/types/db-context-def.ts +15 -15
- package/src/types/db.ts +50 -50
- package/src/types/expr.ts +103 -103
- package/src/types/query-def.ts +50 -50
- package/src/utils/result-parser.ts +88 -88
- package/docs/utilities.md +0 -27
- package/tests/db-context/create-db-context.spec.ts +0 -193
- package/tests/db-context/define-db-context.spec.ts +0 -17
- package/tests/ddl/basic.expected.ts +0 -341
- package/tests/ddl/basic.spec.ts +0 -557
- package/tests/ddl/column-builder.expected.ts +0 -310
- package/tests/ddl/column-builder.spec.ts +0 -525
- package/tests/ddl/index-builder.expected.ts +0 -38
- package/tests/ddl/index-builder.spec.ts +0 -148
- package/tests/ddl/procedure-builder.expected.ts +0 -52
- package/tests/ddl/procedure-builder.spec.ts +0 -128
- package/tests/ddl/relation-builder.expected.ts +0 -36
- package/tests/ddl/relation-builder.spec.ts +0 -171
- package/tests/ddl/table-builder.expected.ts +0 -113
- package/tests/ddl/table-builder.spec.ts +0 -399
- package/tests/ddl/view-builder.expected.ts +0 -38
- package/tests/ddl/view-builder.spec.ts +0 -116
- package/tests/dml/delete.expected.ts +0 -96
- package/tests/dml/delete.spec.ts +0 -127
- package/tests/dml/insert.expected.ts +0 -192
- package/tests/dml/insert.spec.ts +0 -210
- package/tests/dml/update.expected.ts +0 -176
- package/tests/dml/update.spec.ts +0 -222
- package/tests/dml/upsert.expected.ts +0 -215
- package/tests/dml/upsert.spec.ts +0 -190
- package/tests/errors/queryable-errors.spec.ts +0 -126
- package/tests/escape.spec.ts +0 -59
- package/tests/examples/pivot.expected.ts +0 -211
- package/tests/examples/pivot.spec.ts +0 -200
- package/tests/examples/sampling.expected.ts +0 -69
- package/tests/examples/sampling.spec.ts +0 -42
- package/tests/examples/unpivot.expected.ts +0 -120
- package/tests/examples/unpivot.spec.ts +0 -161
- package/tests/exec/search-parser.spec.ts +0 -267
- package/tests/executable/basic.expected.ts +0 -18
- package/tests/executable/basic.spec.ts +0 -54
- package/tests/expr/comparison.expected.ts +0 -282
- package/tests/expr/comparison.spec.ts +0 -334
- package/tests/expr/conditional.expected.ts +0 -134
- package/tests/expr/conditional.spec.ts +0 -249
- package/tests/expr/date.expected.ts +0 -332
- package/tests/expr/date.spec.ts +0 -459
- package/tests/expr/math.expected.ts +0 -62
- package/tests/expr/math.spec.ts +0 -59
- package/tests/expr/string.expected.ts +0 -218
- package/tests/expr/string.spec.ts +0 -300
- package/tests/expr/utility.expected.ts +0 -147
- package/tests/expr/utility.spec.ts +0 -155
- package/tests/select/basic.expected.ts +0 -322
- package/tests/select/basic.spec.ts +0 -433
- package/tests/select/filter.expected.ts +0 -357
- package/tests/select/filter.spec.ts +0 -954
- package/tests/select/group.expected.ts +0 -169
- package/tests/select/group.spec.ts +0 -159
- package/tests/select/join.expected.ts +0 -582
- package/tests/select/join.spec.ts +0 -692
- package/tests/select/order.expected.ts +0 -150
- package/tests/select/order.spec.ts +0 -140
- package/tests/select/recursive-cte.expected.ts +0 -244
- package/tests/select/recursive-cte.spec.ts +0 -514
- package/tests/select/result-meta.spec.ts +0 -270
- package/tests/select/subquery.expected.ts +0 -363
- package/tests/select/subquery.spec.ts +0 -441
- package/tests/select/view.expected.ts +0 -155
- package/tests/select/view.spec.ts +0 -235
- package/tests/select/window.expected.ts +0 -345
- package/tests/select/window.spec.ts +0 -433
- package/tests/setup/MockExecutor.ts +0 -18
- package/tests/setup/TestDbContext.ts +0 -59
- package/tests/setup/models/Company.ts +0 -13
- package/tests/setup/models/Employee.ts +0 -10
- package/tests/setup/models/MonthlySales.ts +0 -11
- package/tests/setup/models/Post.ts +0 -16
- package/tests/setup/models/Sales.ts +0 -10
- package/tests/setup/models/User.ts +0 -19
- package/tests/setup/procedure/GetAllUsers.ts +0 -9
- package/tests/setup/procedure/GetUserById.ts +0 -12
- package/tests/setup/test-utils.ts +0 -72
- package/tests/setup/views/ActiveUsers.ts +0 -8
- package/tests/setup/views/UserSummary.ts +0 -11
- package/tests/types/nullable-queryable-record.spec.ts +0 -97
- package/tests/utils/result-parser-perf.spec.ts +0 -143
- package/tests/utils/result-parser.spec.ts +0 -667
|
@@ -39,20 +39,20 @@ import { MysqlExprRenderer } from "./mysql-expr-renderer";
|
|
|
39
39
|
/**
|
|
40
40
|
* MySQL QueryBuilder
|
|
41
41
|
*
|
|
42
|
-
* MySQL
|
|
43
|
-
* -
|
|
44
|
-
* - INSERT OUTPUT:
|
|
45
|
-
* - UPDATE/UPSERT OUTPUT:
|
|
46
|
-
* - DELETE OUTPUT:
|
|
47
|
-
* - switchFk:
|
|
48
|
-
* -
|
|
42
|
+
* MySQL 고유 사항:
|
|
43
|
+
* - OUTPUT 미지원: multi-statement 패턴으로 우회 (INSERT + SET @var + SELECT)
|
|
44
|
+
* - INSERT OUTPUT: AI column은 LAST_INSERT_ID() 사용, 비 AI는 레코드에서 PK 추출
|
|
45
|
+
* - UPDATE/UPSERT OUTPUT: UPDATE 후 WHERE 조건이 변경될 수 있으므로 먼저 PK를 임시 테이블에 저장 후 SELECT
|
|
46
|
+
* - DELETE OUTPUT: 삭제 전 output column을 임시 테이블에 저장
|
|
47
|
+
* - switchFk: 전역 설정 (SET FOREIGN_KEY_CHECKS), table 파라미터 무시
|
|
48
|
+
* - FK 추가 시 인덱스 자동 생성
|
|
49
49
|
*/
|
|
50
50
|
export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
51
51
|
protected expr = new MysqlExprRenderer((def) => this.select(def).sql);
|
|
52
52
|
|
|
53
53
|
//#region ========== Utilities ==========
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/** 테이블명 렌더링 (MySQL: schema 무시, database.table만 사용) */
|
|
56
56
|
protected tableName(obj: QueryDefObjectName): string {
|
|
57
57
|
if (obj.database != null) {
|
|
58
58
|
return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
|
|
@@ -60,7 +60,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
60
60
|
return this.expr.wrap(obj.name);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
63
|
+
/** LIMIT 절 렌더링 */
|
|
64
64
|
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
65
65
|
if (limit != null) {
|
|
66
66
|
const [offset, count] = limit;
|
|
@@ -75,15 +75,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
75
75
|
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
76
76
|
const alias = this.expr.wrap(join.as);
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// LATERAL JOIN이 필요한지 감지
|
|
79
79
|
if (this.needsLateral(join)) {
|
|
80
|
-
//
|
|
81
|
-
//
|
|
80
|
+
// from이 배열(UNION ALL)이면 renderFrom(join.from) 사용,
|
|
81
|
+
// 그 외(orderBy, top, select 등)이면 renderFrom(join)으로 서브쿼리 생성
|
|
82
82
|
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
83
83
|
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// 일반 JOIN
|
|
87
87
|
const from = this.renderFrom(join.from);
|
|
88
88
|
const where =
|
|
89
89
|
join.where != null && join.where.length > 0
|
|
@@ -128,7 +128,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
128
128
|
|
|
129
129
|
// LOCK
|
|
130
130
|
if (def.lock) {
|
|
131
|
-
// MySQL: SELECT ... FOR UPDATE (
|
|
131
|
+
// MySQL: SELECT ... FOR UPDATE (끝에 추가)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// JOINs
|
|
@@ -149,7 +149,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
149
149
|
// LIMIT
|
|
150
150
|
sql += this.renderLimit(def.limit, def.top);
|
|
151
151
|
|
|
152
|
-
// LOCK (FOR UPDATE
|
|
152
|
+
// LOCK (끝에 FOR UPDATE 추가)
|
|
153
153
|
if (def.lock) {
|
|
154
154
|
sql += " FOR UPDATE";
|
|
155
155
|
}
|
|
@@ -165,13 +165,13 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
165
165
|
const table = this.tableName(def.table);
|
|
166
166
|
|
|
167
167
|
if (def.records.length === 0) {
|
|
168
|
-
throw new Error("INSERT
|
|
168
|
+
throw new Error("INSERT에는 최소 1개의 레코드가 필요합니다.");
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
const columns = Object.keys(def.records[0]);
|
|
172
172
|
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
173
173
|
|
|
174
|
-
//
|
|
174
|
+
// OUTPUT 불필요: 단순 배치 INSERT
|
|
175
175
|
if (def.output == null) {
|
|
176
176
|
const valuesList = def.records.map((record) => {
|
|
177
177
|
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
@@ -180,9 +180,9 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
180
180
|
return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
// OUTPUT
|
|
184
|
-
//
|
|
185
|
-
// →
|
|
183
|
+
// OUTPUT 필요: multi-statement로 INSERT + SELECT 실행
|
|
184
|
+
// 결과 셋: [INSERT 결과, SELECT 결과, INSERT 결과, SELECT 결과, ...]
|
|
185
|
+
// → resultSetIndex=1, resultSetStride=2로 SELECT 결과만 추출
|
|
186
186
|
const output = def.output;
|
|
187
187
|
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
188
188
|
const statements: string[] = [];
|
|
@@ -191,7 +191,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
191
191
|
const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
|
|
192
192
|
statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
|
|
193
193
|
|
|
194
|
-
// SELECT
|
|
194
|
+
// PK로 SELECT (aiColName에는 LAST_INSERT_ID() 사용)
|
|
195
195
|
const whereForSelect = output.pkColNames.map((pk) => {
|
|
196
196
|
const wrappedPk = this.expr.wrap(pk);
|
|
197
197
|
if (pk === output.aiColName) {
|
|
@@ -216,23 +216,23 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
216
216
|
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
217
217
|
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
218
218
|
|
|
219
|
-
//
|
|
219
|
+
// existsSelectQuery를 SELECT 1 AS _로 렌더링
|
|
220
220
|
const existsQuerySql = this.select({
|
|
221
221
|
...def.existsSelectQuery,
|
|
222
222
|
select: { _: { type: "value", value: 1 } },
|
|
223
223
|
}).sql;
|
|
224
224
|
|
|
225
|
-
//
|
|
225
|
+
// OUTPUT 불필요: 단순 INSERT IF NOT EXISTS
|
|
226
226
|
if (def.output == null) {
|
|
227
227
|
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
228
228
|
return { sql };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// OUTPUT
|
|
231
|
+
// OUTPUT 필요: multi-statement (INSERT + SET @affected + SELECT)
|
|
232
232
|
const output = def.output;
|
|
233
233
|
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
234
234
|
|
|
235
|
-
// SELECT WHERE
|
|
235
|
+
// OUTPUT용 SELECT WHERE 조건
|
|
236
236
|
const whereForSelect = output.pkColNames.map((pk) => {
|
|
237
237
|
const wrappedPk = this.expr.wrap(pk);
|
|
238
238
|
if (pk === output.aiColName) {
|
|
@@ -241,14 +241,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
241
241
|
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
-
// multi-statement: INSERT → SET @affected → SELECT (
|
|
244
|
+
// multi-statement: INSERT → SET @affected → SELECT (삽입된 경우에만 결과 반환)
|
|
245
245
|
const statements = [
|
|
246
246
|
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
247
247
|
`SET @sd_affected = ROW_COUNT()`,
|
|
248
248
|
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
|
|
249
249
|
];
|
|
250
250
|
|
|
251
|
-
// results[0]=INSERT, results[1]=SET(
|
|
251
|
+
// results[0]=INSERT, results[1]=SET(빈 결과), results[2]=SELECT
|
|
252
252
|
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -256,7 +256,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
256
256
|
const table = this.tableName(def.table);
|
|
257
257
|
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
258
258
|
|
|
259
|
-
//
|
|
259
|
+
// INSERT INTO SELECT에서 column 추출
|
|
260
260
|
const selectDef = def.recordsSelectQuery;
|
|
261
261
|
const colList =
|
|
262
262
|
selectDef.select != null
|
|
@@ -265,15 +265,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
265
265
|
.join(", ")
|
|
266
266
|
: "*";
|
|
267
267
|
|
|
268
|
-
//
|
|
268
|
+
// OUTPUT 불필요: 단순 INSERT INTO SELECT
|
|
269
269
|
if (def.output == null) {
|
|
270
270
|
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
// OUTPUT
|
|
273
|
+
// OUTPUT 필요: multi-statement
|
|
274
274
|
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
275
275
|
|
|
276
|
-
//
|
|
276
|
+
// PK가 AI인 경우: LAST_INSERT_ID() + ROW_COUNT()로 범위 조회
|
|
277
277
|
if (def.output.aiColName != null) {
|
|
278
278
|
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
279
279
|
|
|
@@ -283,14 +283,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
283
283
|
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
|
|
284
284
|
];
|
|
285
285
|
|
|
286
|
-
// results[0]=INSERT, results[1]=SET(
|
|
286
|
+
// results[0]=INSERT, results[1]=SET(빈 결과), results[2]=SELECT
|
|
287
287
|
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// PK
|
|
290
|
+
// PK가 AI가 아닌 경우: PK를 임시 테이블에 저장 후 조회
|
|
291
291
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
292
292
|
|
|
293
|
-
//
|
|
293
|
+
// recordsSelectQuery에서 PK column만 추출하는 SELECT 생성
|
|
294
294
|
const pkSelectDef: SelectQueryDef = {
|
|
295
295
|
...def.recordsSelectQuery,
|
|
296
296
|
select: Object.fromEntries(
|
|
@@ -299,7 +299,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
299
299
|
};
|
|
300
300
|
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
301
301
|
|
|
302
|
-
//
|
|
302
|
+
// 임시 테이블의 PK를 사용하여 대상에서 SELECT
|
|
303
303
|
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
304
304
|
const wrappedPk = this.expr.wrap(pk);
|
|
305
305
|
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
@@ -312,7 +312,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
312
312
|
`DROP TEMPORARY TABLE ${tempTableName}`,
|
|
313
313
|
];
|
|
314
314
|
|
|
315
|
-
// results[0]=CREATE, results[1]=INSERT, results[2]=SELECT, results[3]=DROP
|
|
315
|
+
// results[0]=CREATE, results[1]=INSERT, results[2]=SELECT, results[3]=DROP 결과
|
|
316
316
|
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
317
317
|
}
|
|
318
318
|
|
|
@@ -329,7 +329,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
329
329
|
([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`,
|
|
330
330
|
);
|
|
331
331
|
|
|
332
|
-
//
|
|
332
|
+
// OUTPUT 불필요: 단순 UPDATE
|
|
333
333
|
if (def.output == null) {
|
|
334
334
|
let sql = `UPDATE ${table} AS ${alias}`;
|
|
335
335
|
sql += this.renderJoins(def.joins);
|
|
@@ -341,11 +341,11 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
341
341
|
return { sql };
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
// OUTPUT
|
|
344
|
+
// OUTPUT 필요: multi-statement (PK를 임시 테이블에 저장 + UPDATE + SELECT + DROP)
|
|
345
345
|
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
346
346
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
347
347
|
|
|
348
|
-
//
|
|
348
|
+
// 대상 PK를 임시 테이블에 저장 (UPDATE 후 WHERE 조건이 변경될 수 있으므로)
|
|
349
349
|
const pkSelectCols = def.output.pkColNames
|
|
350
350
|
.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
|
|
351
351
|
.join(", ");
|
|
@@ -360,14 +360,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
360
360
|
updateSql += this.renderWhere(def.where);
|
|
361
361
|
if (def.top != null) updateSql += ` LIMIT ${def.top}`;
|
|
362
362
|
|
|
363
|
-
//
|
|
363
|
+
// 임시 테이블의 PK로 SELECT (업데이트된 값 조회)
|
|
364
364
|
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
365
365
|
const wrappedPk = this.expr.wrap(pk);
|
|
366
366
|
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
367
367
|
});
|
|
368
368
|
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
369
369
|
|
|
370
|
-
//
|
|
370
|
+
// 임시 테이블 삭제
|
|
371
371
|
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
372
372
|
|
|
373
373
|
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
@@ -382,7 +382,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
382
382
|
const table = this.tableName(def.table);
|
|
383
383
|
const alias = this.expr.wrap(def.as);
|
|
384
384
|
|
|
385
|
-
//
|
|
385
|
+
// OUTPUT 불필요: 단순 DELETE
|
|
386
386
|
if (def.output == null) {
|
|
387
387
|
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
388
388
|
sql += this.renderJoins(def.joins);
|
|
@@ -393,25 +393,25 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
393
393
|
return { sql };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
// OUTPUT
|
|
396
|
+
// OUTPUT 필요: multi-statement (삭제 전 임시 테이블에 저장 + DELETE + SELECT + DROP)
|
|
397
397
|
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
398
398
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
399
399
|
|
|
400
|
-
//
|
|
400
|
+
// 삭제 전 임시 테이블에 저장
|
|
401
401
|
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
|
|
402
402
|
createTempSql += this.renderJoins(def.joins);
|
|
403
403
|
createTempSql += this.renderWhere(def.where);
|
|
404
404
|
|
|
405
|
-
//
|
|
405
|
+
// DELETE 실행
|
|
406
406
|
let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
407
407
|
deleteSql += this.renderJoins(def.joins);
|
|
408
408
|
deleteSql += this.renderWhere(def.where);
|
|
409
409
|
if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
|
|
410
410
|
|
|
411
|
-
//
|
|
411
|
+
// 임시 테이블에서 결과 반환
|
|
412
412
|
const selectSql = `SELECT * FROM ${tempTableName}`;
|
|
413
413
|
|
|
414
|
-
//
|
|
414
|
+
// 임시 테이블 삭제
|
|
415
415
|
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
416
416
|
|
|
417
417
|
const statements = [createTempSql, deleteSql, selectSql, dropSql];
|
|
@@ -427,17 +427,17 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
427
427
|
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
428
428
|
const existsQuerySql = this.select(def.existsSelectQuery).sql;
|
|
429
429
|
|
|
430
|
-
// UPDATE SET
|
|
430
|
+
// UPDATE SET 부분 (alias.column 형식)
|
|
431
431
|
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
432
432
|
([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
433
433
|
);
|
|
434
434
|
|
|
435
|
-
// INSERT
|
|
435
|
+
// INSERT 부분
|
|
436
436
|
const insertColumns = Object.keys(def.insertRecord);
|
|
437
437
|
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
438
438
|
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
439
439
|
|
|
440
|
-
//
|
|
440
|
+
// WHERE 조건 추출 (existsSelectQuery의 where에서)
|
|
441
441
|
const whereCondition =
|
|
442
442
|
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
443
443
|
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
@@ -454,22 +454,22 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
454
454
|
return { sql: statements.join(";\n") };
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
// OUTPUT
|
|
457
|
+
// OUTPUT 필요: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
|
|
458
458
|
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
459
459
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
460
460
|
|
|
461
|
-
//
|
|
461
|
+
// 대상 PK를 임시 테이블에 저장 (UPDATE 후 WHERE 조건이 변경될 수 있으므로)
|
|
462
462
|
const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
|
|
463
463
|
const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
|
|
464
464
|
|
|
465
|
-
// UPDATE (
|
|
465
|
+
// UPDATE (존재하면 업데이트)
|
|
466
466
|
const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
|
|
467
467
|
|
|
468
|
-
// INSERT (NOT EXISTS
|
|
468
|
+
// INSERT (NOT EXISTS 패턴)
|
|
469
469
|
const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
470
470
|
|
|
471
|
-
// SELECT:
|
|
472
|
-
// UPDATE
|
|
471
|
+
// SELECT: UPDATE 결과 또는 INSERT 결과 조회 (UNION ALL로 병합)
|
|
472
|
+
// UPDATE 경우: 임시 테이블의 PK로 조회
|
|
473
473
|
const output = def.output;
|
|
474
474
|
const updatePkConditions = output.pkColNames.map((pk) => {
|
|
475
475
|
const wrappedPk = this.expr.wrap(pk);
|
|
@@ -477,7 +477,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
477
477
|
});
|
|
478
478
|
const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
|
|
479
479
|
|
|
480
|
-
// INSERT
|
|
480
|
+
// INSERT 경우: insertRecord의 PK로 조회 (AI에는 LAST_INSERT_ID() 사용, 임시 테이블이 비어있을 때만)
|
|
481
481
|
const insertPkConditions = output.pkColNames.map((pk) => {
|
|
482
482
|
const wrappedPk = this.expr.wrap(pk);
|
|
483
483
|
if (pk === output.aiColName) {
|
|
@@ -490,7 +490,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
490
490
|
|
|
491
491
|
const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
|
|
492
492
|
|
|
493
|
-
//
|
|
493
|
+
// 임시 테이블 삭제
|
|
494
494
|
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
495
495
|
|
|
496
496
|
const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
|
|
@@ -507,7 +507,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
507
507
|
const colDefs = def.columns.map((col) => {
|
|
508
508
|
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
509
509
|
|
|
510
|
-
// nullable: true → NULL,
|
|
510
|
+
// nullable: true → NULL, 아니면 → NOT NULL
|
|
511
511
|
if (col.nullable === true) {
|
|
512
512
|
colSql += " NULL";
|
|
513
513
|
} else {
|
|
@@ -525,7 +525,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
525
525
|
return colSql;
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
-
//
|
|
528
|
+
// CONSTRAINT 이름이 포함된 Primary Key
|
|
529
529
|
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
530
530
|
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
531
531
|
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
@@ -544,7 +544,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
544
544
|
}
|
|
545
545
|
|
|
546
546
|
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
547
|
-
// MySQL: TRUNCATE
|
|
547
|
+
// MySQL: TRUNCATE는 AUTO_INCREMENT를 자동으로 초기화
|
|
548
548
|
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -608,7 +608,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
608
608
|
|
|
609
609
|
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
610
610
|
const table = this.tableName(def.table);
|
|
611
|
-
// MySQL 8.0+: RENAME COLUMN
|
|
611
|
+
// MySQL 8.0+: RENAME COLUMN 지원
|
|
612
612
|
return {
|
|
613
613
|
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
614
614
|
};
|
|
@@ -635,7 +635,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
635
635
|
const targetTable = this.tableName(fk.targetTable);
|
|
636
636
|
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
637
637
|
|
|
638
|
-
// MySQL
|
|
638
|
+
// MySQL은 FK 추가 시 인덱스를 자동 생성하므로 별도 인덱스 불필요
|
|
639
639
|
return {
|
|
640
640
|
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
|
|
641
641
|
};
|
|
@@ -676,7 +676,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
676
676
|
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
677
677
|
const proc = this.tableName(def.procedure);
|
|
678
678
|
|
|
679
|
-
//
|
|
679
|
+
// 파라미터 처리
|
|
680
680
|
const paramList =
|
|
681
681
|
def.params
|
|
682
682
|
?.map((p) => {
|
|
@@ -719,11 +719,11 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
719
719
|
//#region ========== Utils ==========
|
|
720
720
|
|
|
721
721
|
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
722
|
-
// MySQL:
|
|
723
|
-
//
|
|
724
|
-
// SQL
|
|
722
|
+
// MySQL: 모든 테이블 삭제 (MySQL에서 database와 schema는 동의어)
|
|
723
|
+
// information_schema에서 테이블 목록 조회 후 DROP
|
|
724
|
+
// SQL 인젝션 방지: 식별자 유효성 검사
|
|
725
725
|
if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
|
|
726
|
-
throw new Error(
|
|
726
|
+
throw new Error(`잘못된 데이터베이스 이름: ${def.database}`);
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
const dbName = this.expr.escapeString(def.database);
|
|
@@ -741,14 +741,14 @@ SET FOREIGN_KEY_CHECKS = 1`,
|
|
|
741
741
|
}
|
|
742
742
|
|
|
743
743
|
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
744
|
-
// MySQL: database
|
|
744
|
+
// MySQL: database와 schema는 동의어
|
|
745
745
|
const dbName = this.expr.escapeString(def.database);
|
|
746
746
|
return {
|
|
747
747
|
sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
|
|
748
748
|
};
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
/** MySQL
|
|
751
|
+
/** MySQL은 전역 설정만 지원 (table 파라미터 무시) */
|
|
752
752
|
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
753
753
|
return def.enabled
|
|
754
754
|
? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
|
|
@@ -69,22 +69,22 @@ import type { DataType } from "../../types/column";
|
|
|
69
69
|
import { ExprRendererBase } from "../base/expr-renderer-base";
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* PostgreSQL
|
|
72
|
+
* PostgreSQL 표현식 렌더러
|
|
73
73
|
*/
|
|
74
74
|
export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
75
|
-
//#region ==========
|
|
75
|
+
//#region ========== 유틸리티 (public - QueryBuilder에서도 사용) ==========
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/** 식별자 감싸기 */
|
|
78
78
|
wrap(name: string): string {
|
|
79
79
|
return `"${name.replace(/"/g, '""')}"`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
82
|
+
/** SQL 문자열 리터럴용 이스케이프 (따옴표 제외) */
|
|
83
83
|
escapeString(value: string): string {
|
|
84
84
|
return value.replace(/'/g, "''");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
87
|
+
/** 값 이스케이프 */
|
|
88
88
|
escapeValue(value: unknown): string {
|
|
89
89
|
if (value == null) {
|
|
90
90
|
return "NULL";
|
|
@@ -113,10 +113,10 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
113
113
|
if (value instanceof Uint8Array) {
|
|
114
114
|
return `'\\x${bytes.toHex(value)}'::bytea`;
|
|
115
115
|
}
|
|
116
|
-
throw new Error(
|
|
116
|
+
throw new Error(`알 수 없는 값 타입: ${typeof value}`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/** DataType → SQL
|
|
119
|
+
/** DataType → SQL 타입 변환 */
|
|
120
120
|
renderDataType(dataType: DataType): string {
|
|
121
121
|
switch (dataType.type) {
|
|
122
122
|
case "int":
|
|
@@ -176,7 +176,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
176
176
|
//#region ========== comparison (null-safe) ==========
|
|
177
177
|
|
|
178
178
|
protected eq(expr: ExprEq): string {
|
|
179
|
-
// PostgreSQL:
|
|
179
|
+
// PostgreSQL: NULL 안전 동등 비교 (IS NOT DISTINCT FROM 연산자 사용)
|
|
180
180
|
const left = this.render(expr.source);
|
|
181
181
|
const right = this.render(expr.target);
|
|
182
182
|
return `${left} IS NOT DISTINCT FROM ${right}`;
|
|
@@ -217,7 +217,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
protected like(expr: ExprLike): string {
|
|
220
|
-
//
|
|
220
|
+
// 항상 ESCAPE '\' 추가
|
|
221
221
|
return `${this.render(expr.source)} LIKE ${this.render(expr.pattern)} ESCAPE '\\'`;
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -228,7 +228,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
228
228
|
|
|
229
229
|
protected in(expr: ExprIn): string {
|
|
230
230
|
if (expr.values.length === 0) {
|
|
231
|
-
return "FALSE"; //
|
|
231
|
+
return "FALSE"; // 빈 IN은 항상 false
|
|
232
232
|
}
|
|
233
233
|
const values = expr.values.map((v) => this.render(v)).join(", ");
|
|
234
234
|
return `${this.render(expr.source)} IN (${values})`;
|
|
@@ -239,7 +239,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
protected exists(expr: ExprExists): string {
|
|
242
|
-
//
|
|
242
|
+
// SELECT 1로 렌더링
|
|
243
243
|
const subquery = this.buildSelect({
|
|
244
244
|
...expr.query,
|
|
245
245
|
select: { _: { type: "value", value: 1 } },
|
|
@@ -270,7 +270,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
270
270
|
//#region ========== String (null handling) ==========
|
|
271
271
|
|
|
272
272
|
protected concat(expr: ExprConcat): string {
|
|
273
|
-
// PostgreSQL:
|
|
273
|
+
// PostgreSQL: COALESCE와 || 연산자 사용
|
|
274
274
|
const args = expr.args.map((a) => `COALESCE(${this.render(a)}, '')`);
|
|
275
275
|
return args.join(" || ");
|
|
276
276
|
}
|
|
@@ -304,12 +304,12 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
protected length(expr: ExprLength): string {
|
|
307
|
-
// PostgreSQL: LENGTH() (null
|
|
307
|
+
// PostgreSQL: LENGTH() (null 처리)
|
|
308
308
|
return `LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
protected byteLength(expr: ExprByteLength): string {
|
|
312
|
-
// PostgreSQL: OCTET_LENGTH() (null
|
|
312
|
+
// PostgreSQL: OCTET_LENGTH() (null 처리)
|
|
313
313
|
return `OCTET_LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
|
|
314
314
|
}
|
|
315
315
|
|
|
@@ -378,7 +378,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
378
378
|
|
|
379
379
|
protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
|
|
380
380
|
const src = this.render(expr.arg);
|
|
381
|
-
// ISO
|
|
381
|
+
// ISO 주 시작일 (월요일)
|
|
382
382
|
return `DATE_TRUNC('week', ${src})::DATE`;
|
|
383
383
|
}
|
|
384
384
|
|
|
@@ -413,7 +413,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
protected formatDate(expr: ExprFormatDate): string {
|
|
416
|
-
// JS
|
|
416
|
+
// JS 포맷 → PostgreSQL TO_CHAR 포맷
|
|
417
417
|
const pgFormat = this.convertDateFormat(expr.format);
|
|
418
418
|
return `TO_CHAR(${this.render(expr.source)}, '${pgFormat}')`;
|
|
419
419
|
}
|
|
@@ -436,7 +436,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
private convertDateFormat(format: string): string {
|
|
439
|
-
// JS
|
|
439
|
+
// JS 포맷 → PostgreSQL TO_CHAR 포맷
|
|
440
440
|
return format
|
|
441
441
|
.replace(/yyyy/g, "YYYY")
|
|
442
442
|
.replace(/MM/g, "MM")
|
|
@@ -510,14 +510,14 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
510
510
|
//#region ========== Other ==========
|
|
511
511
|
|
|
512
512
|
protected greatest(expr: ExprGreatest): string {
|
|
513
|
-
if (expr.args.length === 0) throw new Error("greatest
|
|
514
|
-
// PostgreSQL:
|
|
513
|
+
if (expr.args.length === 0) throw new Error("greatest에는 최소 1개의 인수가 필요합니다.");
|
|
514
|
+
// PostgreSQL: GREATEST 네이티브 지원
|
|
515
515
|
return `GREATEST(${expr.args.map((a) => this.render(a)).join(", ")})`;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
protected least(expr: ExprLeast): string {
|
|
519
|
-
if (expr.args.length === 0) throw new Error("least
|
|
520
|
-
// PostgreSQL:
|
|
519
|
+
if (expr.args.length === 0) throw new Error("least에는 최소 1개의 인수가 필요합니다.");
|
|
520
|
+
// PostgreSQL: LEAST 네이티브 지원
|
|
521
521
|
return `LEAST(${expr.args.map((a) => this.render(a)).join(", ")})`;
|
|
522
522
|
}
|
|
523
523
|
|
|
@@ -541,7 +541,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
541
541
|
const fn = this.renderWindowFn(expr.fn);
|
|
542
542
|
let over = this.renderWindowSpec(expr.spec);
|
|
543
543
|
|
|
544
|
-
// LAST_VALUE
|
|
544
|
+
// LAST_VALUE 기본 프레임은 CURRENT ROW까지만 보므로 전체 프레임을 지정해야 함
|
|
545
545
|
if (expr.fn.type === "lastValue" && over.length > 0) {
|
|
546
546
|
over += " ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING";
|
|
547
547
|
}
|