@simplysm/orm-common 13.0.68 → 13.0.70
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 +54 -1447
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +9 -9
- package/dist/create-db-context.js.map +1 -1
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +2 -2
- package/dist/ddl/initialize.js.map +1 -1
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/table-ddl.d.ts +24 -24
- package/dist/ddl/table-ddl.js +4 -4
- package/dist/ddl/table-ddl.js.map +1 -1
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +3 -3
- package/dist/exec/executable.js.map +1 -1
- package/dist/exec/queryable.d.ts +160 -160
- package/dist/exec/queryable.js +119 -119
- package/dist/exec/queryable.js.map +1 -1
- package/dist/exec/search-parser.d.ts +37 -37
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr.d.ts +257 -257
- package/dist/expr/expr.js +265 -265
- package/dist/expr/expr.js.map +1 -1
- package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
- package/dist/query-builder/base/expr-renderer-base.js +2 -2
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
- package/dist/query-builder/base/query-builder-base.d.ts +26 -26
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +22 -22
- package/dist/query-builder/base/query-builder-base.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- 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 +11 -11
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/schema/factory/column-builder.d.ts +79 -79
- package/dist/schema/factory/column-builder.js +42 -42
- package/dist/schema/factory/index-builder.d.ts +39 -39
- package/dist/schema/factory/index-builder.js +26 -26
- 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 +38 -38
- package/dist/schema/procedure-builder.d.ts +49 -49
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +33 -33
- package/dist/schema/table-builder.d.ts +59 -59
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +43 -43
- package/dist/schema/view-builder.d.ts +49 -49
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +32 -32
- package/dist/types/column.d.ts +22 -22
- package/dist/types/column.js +1 -1
- package/dist/types/column.js.map +1 -1
- package/dist/types/db.d.ts +40 -40
- package/dist/types/expr.d.ts +59 -59
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/query-def.d.ts +44 -44
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +3 -3
- package/dist/utils/result-parser.js.map +1 -1
- package/package.json +5 -5
- package/src/create-db-context.ts +20 -20
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +259 -259
- package/src/ddl/relation-ddl.ts +89 -89
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +189 -189
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +2033 -2033
- package/src/exec/search-parser.ts +57 -57
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +2140 -2140
- package/src/query-builder/base/expr-renderer-base.ts +237 -237
- package/src/query-builder/base/query-builder-base.ts +213 -213
- package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
- package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
- package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
- package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
- package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
- package/src/query-builder/query-builder.ts +19 -19
- package/src/schema/factory/column-builder.ts +423 -423
- package/src/schema/factory/index-builder.ts +164 -164
- package/src/schema/factory/relation-builder.ts +453 -453
- package/src/schema/procedure-builder.ts +232 -232
- package/src/schema/table-builder.ts +319 -319
- package/src/schema/view-builder.ts +221 -221
- package/src/types/column.ts +188 -188
- package/src/types/db.ts +208 -208
- package/src/types/expr.ts +697 -697
- package/src/types/query-def.ts +513 -513
- package/src/utils/result-parser.ts +458 -458
- package/tests/db-context/create-db-context.spec.ts +224 -0
- package/tests/db-context/define-db-context.spec.ts +68 -0
- package/tests/ddl/basic.expected.ts +341 -0
- package/tests/ddl/basic.spec.ts +714 -0
- package/tests/ddl/column-builder.expected.ts +310 -0
- package/tests/ddl/column-builder.spec.ts +637 -0
- package/tests/ddl/index-builder.expected.ts +38 -0
- package/tests/ddl/index-builder.spec.ts +202 -0
- package/tests/ddl/procedure-builder.expected.ts +52 -0
- package/tests/ddl/procedure-builder.spec.ts +234 -0
- package/tests/ddl/relation-builder.expected.ts +36 -0
- package/tests/ddl/relation-builder.spec.ts +372 -0
- package/tests/ddl/table-builder.expected.ts +113 -0
- package/tests/ddl/table-builder.spec.ts +433 -0
- package/tests/ddl/view-builder.expected.ts +38 -0
- package/tests/ddl/view-builder.spec.ts +176 -0
- package/tests/dml/delete.expected.ts +96 -0
- package/tests/dml/delete.spec.ts +160 -0
- package/tests/dml/insert.expected.ts +192 -0
- package/tests/dml/insert.spec.ts +288 -0
- package/tests/dml/update.expected.ts +176 -0
- package/tests/dml/update.spec.ts +318 -0
- package/tests/dml/upsert.expected.ts +215 -0
- package/tests/dml/upsert.spec.ts +242 -0
- package/tests/errors/queryable-errors.spec.ts +177 -0
- package/tests/escape.spec.ts +100 -0
- package/tests/examples/pivot.expected.ts +211 -0
- package/tests/examples/pivot.spec.ts +533 -0
- package/tests/examples/sampling.expected.ts +69 -0
- package/tests/examples/sampling.spec.ts +104 -0
- package/tests/examples/unpivot.expected.ts +120 -0
- package/tests/examples/unpivot.spec.ts +226 -0
- package/tests/exec/search-parser.spec.ts +283 -0
- package/tests/executable/basic.expected.ts +18 -0
- package/tests/executable/basic.spec.ts +54 -0
- package/tests/expr/comparison.expected.ts +282 -0
- package/tests/expr/comparison.spec.ts +400 -0
- package/tests/expr/conditional.expected.ts +134 -0
- package/tests/expr/conditional.spec.ts +276 -0
- package/tests/expr/date.expected.ts +332 -0
- package/tests/expr/date.spec.ts +526 -0
- package/tests/expr/math.expected.ts +62 -0
- package/tests/expr/math.spec.ts +106 -0
- package/tests/expr/string.expected.ts +218 -0
- package/tests/expr/string.spec.ts +356 -0
- package/tests/expr/utility.expected.ts +147 -0
- package/tests/expr/utility.spec.ts +182 -0
- package/tests/select/basic.expected.ts +322 -0
- package/tests/select/basic.spec.ts +502 -0
- package/tests/select/filter.expected.ts +357 -0
- package/tests/select/filter.spec.ts +1068 -0
- package/tests/select/group.expected.ts +169 -0
- package/tests/select/group.spec.ts +244 -0
- package/tests/select/join.expected.ts +582 -0
- package/tests/select/join.spec.ts +805 -0
- package/tests/select/order.expected.ts +150 -0
- package/tests/select/order.spec.ts +189 -0
- package/tests/select/recursive-cte.expected.ts +244 -0
- package/tests/select/recursive-cte.spec.ts +514 -0
- package/tests/select/result-meta.spec.ts +270 -0
- package/tests/select/subquery.expected.ts +363 -0
- package/tests/select/subquery.spec.ts +537 -0
- package/tests/select/view.expected.ts +155 -0
- package/tests/select/view.spec.ts +235 -0
- package/tests/select/window.expected.ts +345 -0
- package/tests/select/window.spec.ts +618 -0
- package/tests/setup/MockExecutor.ts +18 -0
- package/tests/setup/TestDbContext.ts +59 -0
- package/tests/setup/models/Company.ts +13 -0
- package/tests/setup/models/Employee.ts +10 -0
- package/tests/setup/models/MonthlySales.ts +11 -0
- package/tests/setup/models/Post.ts +16 -0
- package/tests/setup/models/Sales.ts +10 -0
- package/tests/setup/models/User.ts +19 -0
- package/tests/setup/procedure/GetAllUsers.ts +9 -0
- package/tests/setup/procedure/GetUserById.ts +12 -0
- package/tests/setup/test-utils.ts +72 -0
- package/tests/setup/views/ActiveUsers.ts +8 -0
- package/tests/setup/views/UserSummary.ts +11 -0
- package/tests/types/nullable-queryable-record.spec.ts +145 -0
- package/tests/utils/result-parser-perf.spec.ts +210 -0
- package/tests/utils/result-parser.spec.ts +701 -0
- package/docs/expressions.md +0 -172
- package/docs/queries.md +0 -444
- package/docs/schema.md +0 -245
|
@@ -1,686 +1,686 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AddColumnQueryDef,
|
|
3
|
-
AddFkQueryDef,
|
|
4
|
-
AddIdxQueryDef,
|
|
5
|
-
AddPkQueryDef,
|
|
6
|
-
ClearSchemaQueryDef,
|
|
7
|
-
CreateProcQueryDef,
|
|
8
|
-
CreateTableQueryDef,
|
|
9
|
-
CreateViewQueryDef,
|
|
10
|
-
SchemaExistsQueryDef,
|
|
11
|
-
DeleteQueryDef,
|
|
12
|
-
DropColumnQueryDef,
|
|
13
|
-
DropFkQueryDef,
|
|
14
|
-
DropIdxQueryDef,
|
|
15
|
-
DropPkQueryDef,
|
|
16
|
-
DropProcQueryDef,
|
|
17
|
-
DropTableQueryDef,
|
|
18
|
-
DropViewQueryDef,
|
|
19
|
-
ExecProcQueryDef,
|
|
20
|
-
InsertIfNotExistsQueryDef,
|
|
21
|
-
InsertIntoQueryDef,
|
|
22
|
-
InsertQueryDef,
|
|
23
|
-
ModifyColumnQueryDef,
|
|
24
|
-
QueryDefObjectName,
|
|
25
|
-
RenameColumnQueryDef,
|
|
26
|
-
RenameTableQueryDef,
|
|
27
|
-
SelectQueryDef,
|
|
28
|
-
SelectQueryDefJoin,
|
|
29
|
-
SwitchFkQueryDef,
|
|
30
|
-
TruncateQueryDef,
|
|
31
|
-
UpdateQueryDef,
|
|
32
|
-
UpsertQueryDef,
|
|
33
|
-
} from "../../types/query-def";
|
|
34
|
-
import type { QueryBuildResult } from "../../types/db";
|
|
35
|
-
import { QueryBuilderBase } from "../base/query-builder-base";
|
|
36
|
-
import { PostgresqlExprRenderer } from "./postgresql-expr-renderer";
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* PostgreSQL QueryBuilder
|
|
40
|
-
*
|
|
41
|
-
* PostgreSQL 특이사항:
|
|
42
|
-
* - OUTPUT: RETURNING 절 사용 (네이티브 지원)
|
|
43
|
-
* - TRUNCATE: RESTART IDENTITY
|
|
44
|
-
* - UPSERT: CTE 방식 (INSERT ... ON CONFLICT는 단일 unique 제약만 지원)
|
|
45
|
-
* - AUTO_INCREMENT: GENERATED BY DEFAULT AS IDENTITY (
|
|
46
|
-
* - FK
|
|
47
|
-
*/
|
|
48
|
-
export class PostgresqlQueryBuilder extends QueryBuilderBase {
|
|
49
|
-
protected expr = new PostgresqlExprRenderer((def) => this.select(def).sql);
|
|
50
|
-
|
|
51
|
-
//#region ========== 유틸리티 ==========
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
protected tableName(obj: QueryDefObjectName): string {
|
|
55
|
-
const schema = obj.schema ?? "public";
|
|
56
|
-
return `${this.expr.wrap(schema)}.${this.expr.wrap(obj.name)}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** LIMIT...OFFSET 절
|
|
60
|
-
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
61
|
-
if (limit != null) {
|
|
62
|
-
const [offset, count] = limit;
|
|
63
|
-
return ` LIMIT ${count} OFFSET ${offset}`;
|
|
64
|
-
}
|
|
65
|
-
if (top != null) {
|
|
66
|
-
return ` LIMIT ${top}`;
|
|
67
|
-
}
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
72
|
-
const alias = this.expr.wrap(join.as);
|
|
73
|
-
|
|
74
|
-
// LATERAL JOIN 필요 여부 감지
|
|
75
|
-
if (this.needsLateral(join)) {
|
|
76
|
-
// from이 배열(UNION ALL)이면 renderFrom(join.from),
|
|
77
|
-
// 그 외(orderBy, top, select 등)면 renderFrom(join)으로
|
|
78
|
-
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
79
|
-
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 일반 JOIN
|
|
83
|
-
const from = this.renderFrom(join.from);
|
|
84
|
-
const where =
|
|
85
|
-
join.where != null && join.where.length > 0
|
|
86
|
-
? ` ON ${this.expr.renderWhere(join.where)}`
|
|
87
|
-
: " ON TRUE";
|
|
88
|
-
return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
|
|
93
|
-
//#region ========== DML - SELECT ==========
|
|
94
|
-
|
|
95
|
-
protected select(def: SelectQueryDef): QueryBuildResult {
|
|
96
|
-
// WITH (CTE)
|
|
97
|
-
let sql = "";
|
|
98
|
-
if (def.with != null) {
|
|
99
|
-
const { name, base, recursive } = def.with;
|
|
100
|
-
sql += `WITH RECURSIVE ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// SELECT
|
|
104
|
-
sql += "SELECT";
|
|
105
|
-
if (def.distinct) {
|
|
106
|
-
sql += " DISTINCT";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// columns
|
|
110
|
-
if (def.select != null) {
|
|
111
|
-
const cols = Object.entries(def.select).map(
|
|
112
|
-
([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
|
|
113
|
-
);
|
|
114
|
-
sql += ` ${cols.join(", ")}`;
|
|
115
|
-
} else {
|
|
116
|
-
sql += " *";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// FROM
|
|
120
|
-
if (def.from != null) {
|
|
121
|
-
const from = this.renderFrom(def.from);
|
|
122
|
-
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// JOINs
|
|
126
|
-
sql += this.renderJoins(def.joins);
|
|
127
|
-
|
|
128
|
-
// WHERE
|
|
129
|
-
sql += this.renderWhere(def.where);
|
|
130
|
-
|
|
131
|
-
// GROUP BY
|
|
132
|
-
sql += this.renderGroupBy(def.groupBy);
|
|
133
|
-
|
|
134
|
-
// HAVING
|
|
135
|
-
sql += this.renderHaving(def.having);
|
|
136
|
-
|
|
137
|
-
// ORDER BY
|
|
138
|
-
sql += this.renderOrderBy(def.orderBy);
|
|
139
|
-
|
|
140
|
-
// LIMIT
|
|
141
|
-
sql += this.renderLimit(def.limit, def.top);
|
|
142
|
-
|
|
143
|
-
// LOCK (FOR UPDATE at end)
|
|
144
|
-
if (def.lock) {
|
|
145
|
-
sql += " FOR UPDATE";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return { sql };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
//#endregion
|
|
152
|
-
|
|
153
|
-
//#region ========== DML - INSERT ==========
|
|
154
|
-
|
|
155
|
-
protected insert(def: InsertQueryDef): QueryBuildResult {
|
|
156
|
-
const table = this.tableName(def.table);
|
|
157
|
-
|
|
158
|
-
if (def.records.length === 0) {
|
|
159
|
-
throw new Error("INSERT
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const columns = Object.keys(def.records[0]);
|
|
163
|
-
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
164
|
-
|
|
165
|
-
const valuesList = def.records.map((record) => {
|
|
166
|
-
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
167
|
-
return `(${values.join(", ")})`;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
let sql = `INSERT INTO ${table} (${colList})`;
|
|
171
|
-
|
|
172
|
-
// GENERATED BY DEFAULT AS IDENTITY이므로
|
|
173
|
-
// overrideIdentity 파라미터는 MSSQL(SET IDENTITY_INSERT) 호환성을 위해 유지되지만
|
|
174
|
-
// PostgreSQL에서는 GENERATED BY DEFAULT가
|
|
175
|
-
// (참고: GENERATED ALWAYS였다면 OVERRIDING SYSTEM VALUE 필요)
|
|
176
|
-
|
|
177
|
-
sql += ` VALUES ${valuesList.join(", ")}`;
|
|
178
|
-
|
|
179
|
-
// RETURNING (PostgreSQL 네이티브 지원)
|
|
180
|
-
if (def.output != null) {
|
|
181
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
182
|
-
sql += ` RETURNING ${outputCols}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return { sql };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
|
|
189
|
-
const table = this.tableName(def.table);
|
|
190
|
-
|
|
191
|
-
const columns = Object.keys(def.record);
|
|
192
|
-
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
193
|
-
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
194
|
-
|
|
195
|
-
// existsSelectQuery를 SELECT 1 AS _ 형태로
|
|
196
|
-
const existsQuerySql = this.select({
|
|
197
|
-
...def.existsSelectQuery,
|
|
198
|
-
select: { _: { type: "value", value: 1 } },
|
|
199
|
-
}).sql;
|
|
200
|
-
|
|
201
|
-
let sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
202
|
-
|
|
203
|
-
// RETURNING
|
|
204
|
-
if (def.output != null) {
|
|
205
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
206
|
-
sql += ` RETURNING ${outputCols}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { sql };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
|
|
213
|
-
const table = this.tableName(def.table);
|
|
214
|
-
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
215
|
-
|
|
216
|
-
// INSERT INTO SELECT에서 columns 추출
|
|
217
|
-
const selectDef = def.recordsSelectQuery;
|
|
218
|
-
const colList =
|
|
219
|
-
selectDef.select != null
|
|
220
|
-
? Object.keys(selectDef.select)
|
|
221
|
-
.map((c) => this.expr.wrap(c))
|
|
222
|
-
.join(", ")
|
|
223
|
-
: "*";
|
|
224
|
-
|
|
225
|
-
let sql = `INSERT INTO ${table} (${colList}) ${selectSql}`;
|
|
226
|
-
|
|
227
|
-
// RETURNING
|
|
228
|
-
if (def.output != null) {
|
|
229
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
230
|
-
sql += ` RETURNING ${outputCols}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return { sql };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
//#endregion
|
|
237
|
-
|
|
238
|
-
//#region ========== DML - UPDATE ==========
|
|
239
|
-
|
|
240
|
-
protected update(def: UpdateQueryDef): QueryBuildResult {
|
|
241
|
-
const table = this.tableName(def.table);
|
|
242
|
-
const alias = this.expr.wrap(def.as);
|
|
243
|
-
|
|
244
|
-
// SET
|
|
245
|
-
const setParts = Object.entries(def.record).map(
|
|
246
|
-
([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
let sql = `UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
|
|
250
|
-
|
|
251
|
-
// PostgreSQL: JOIN은 FROM 절로
|
|
252
|
-
if (def.joins != null && def.joins.length > 0) {
|
|
253
|
-
const joinTables = def.joins.map((j) => {
|
|
254
|
-
const from = this.renderFrom(j.from);
|
|
255
|
-
return `${from} AS ${this.expr.wrap(j.as)}`;
|
|
256
|
-
});
|
|
257
|
-
sql += ` FROM ${joinTables.join(", ")}`;
|
|
258
|
-
|
|
259
|
-
// JOIN ON 조건을 WHERE에
|
|
260
|
-
const joinConditions = def.joins
|
|
261
|
-
.filter((j) => j.where != null && j.where.length > 0)
|
|
262
|
-
.map((j) => this.expr.renderWhere(j.where!));
|
|
263
|
-
if (joinConditions.length > 0) {
|
|
264
|
-
const whereCondition =
|
|
265
|
-
def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
|
|
266
|
-
const allConditions =
|
|
267
|
-
whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
|
|
268
|
-
sql += ` WHERE ${allConditions.join(" AND ")}`;
|
|
269
|
-
} else {
|
|
270
|
-
sql += this.renderWhere(def.where);
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
sql += this.renderWhere(def.where);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// RETURNING
|
|
277
|
-
if (def.output != null) {
|
|
278
|
-
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
279
|
-
sql += ` RETURNING ${outputCols}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return { sql };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
//#endregion
|
|
286
|
-
|
|
287
|
-
//#region ========== DML - DELETE ==========
|
|
288
|
-
|
|
289
|
-
protected delete(def: DeleteQueryDef): QueryBuildResult {
|
|
290
|
-
const table = this.tableName(def.table);
|
|
291
|
-
const alias = this.expr.wrap(def.as);
|
|
292
|
-
|
|
293
|
-
let sql = `DELETE FROM ${table} AS ${alias}`;
|
|
294
|
-
|
|
295
|
-
// PostgreSQL: JOIN은 USING 절로
|
|
296
|
-
if (def.joins != null && def.joins.length > 0) {
|
|
297
|
-
const joinTables = def.joins.map((j) => {
|
|
298
|
-
const from = this.renderFrom(j.from);
|
|
299
|
-
return `${from} AS ${this.expr.wrap(j.as)}`;
|
|
300
|
-
});
|
|
301
|
-
sql += ` USING ${joinTables.join(", ")}`;
|
|
302
|
-
|
|
303
|
-
// JOIN ON 조건을 WHERE에
|
|
304
|
-
const joinConditions = def.joins
|
|
305
|
-
.filter((j) => j.where != null && j.where.length > 0)
|
|
306
|
-
.map((j) => this.expr.renderWhere(j.where!));
|
|
307
|
-
if (joinConditions.length > 0) {
|
|
308
|
-
const whereCondition =
|
|
309
|
-
def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
|
|
310
|
-
const allConditions =
|
|
311
|
-
whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
|
|
312
|
-
sql += ` WHERE ${allConditions.join(" AND ")}`;
|
|
313
|
-
} else {
|
|
314
|
-
sql += this.renderWhere(def.where);
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
sql += this.renderWhere(def.where);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// RETURNING (PostgreSQL: DELETE에서도 지원)
|
|
321
|
-
if (def.output != null) {
|
|
322
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
323
|
-
sql += ` RETURNING ${outputCols}`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return { sql };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
//#endregion
|
|
330
|
-
|
|
331
|
-
//#region ========== DML - UPSERT ==========
|
|
332
|
-
|
|
333
|
-
protected upsert(def: UpsertQueryDef): QueryBuildResult {
|
|
334
|
-
// PostgreSQL: CTE 방식 (ON CONFLICT는 단일 unique 제약만 지원하므로 범용성 위해 CTE 사용)
|
|
335
|
-
const table = this.tableName(def.table);
|
|
336
|
-
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
337
|
-
|
|
338
|
-
// UPDATE SET
|
|
339
|
-
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
340
|
-
([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
// INSERT
|
|
344
|
-
const insertColumns = Object.keys(def.insertRecord);
|
|
345
|
-
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
346
|
-
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
347
|
-
|
|
348
|
-
// WHERE
|
|
349
|
-
const whereCondition =
|
|
350
|
-
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
351
|
-
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
352
|
-
: "TRUE";
|
|
353
|
-
|
|
354
|
-
// OUTPUT
|
|
355
|
-
const outputCols =
|
|
356
|
-
def.output != null ? def.output.columns.map((c) => this.expr.wrap(c)).join(", ") : "*";
|
|
357
|
-
|
|
358
|
-
// CTE 방식 UPSERT
|
|
359
|
-
let sql = `WITH matched AS (\n`;
|
|
360
|
-
sql += ` SELECT ${alias}.* FROM ${table} AS ${alias} WHERE ${whereCondition}\n`;
|
|
361
|
-
sql += `),\n`;
|
|
362
|
-
sql += `updated AS (\n`;
|
|
363
|
-
sql += ` UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")}\n`;
|
|
364
|
-
sql += ` WHERE ${whereCondition}\n`;
|
|
365
|
-
sql += ` RETURNING ${outputCols}\n`;
|
|
366
|
-
sql += `),\n`;
|
|
367
|
-
sql += `inserted AS (\n`;
|
|
368
|
-
sql += ` INSERT INTO ${table} (${insertColList})\n`;
|
|
369
|
-
sql += ` SELECT ${insertValues}\n`;
|
|
370
|
-
sql += ` WHERE NOT EXISTS (SELECT 1 FROM matched)\n`;
|
|
371
|
-
sql += ` RETURNING ${outputCols}\n`;
|
|
372
|
-
sql += `)\n`;
|
|
373
|
-
sql += `SELECT * FROM updated UNION ALL SELECT * FROM inserted`;
|
|
374
|
-
|
|
375
|
-
return { sql };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
//#endregion
|
|
379
|
-
|
|
380
|
-
//#region ========== DDL - Table ==========
|
|
381
|
-
|
|
382
|
-
protected createTable(def: CreateTableQueryDef): QueryBuildResult {
|
|
383
|
-
const table = this.tableName(def.table);
|
|
384
|
-
|
|
385
|
-
const colDefs = def.columns.map((col) => {
|
|
386
|
-
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
387
|
-
|
|
388
|
-
// nullable: true → NULL, else → NOT NULL
|
|
389
|
-
if (col.nullable === true) {
|
|
390
|
-
colSql += " NULL";
|
|
391
|
-
} else {
|
|
392
|
-
colSql += " NOT NULL";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (col.autoIncrement) {
|
|
396
|
-
colSql += " GENERATED BY DEFAULT AS IDENTITY";
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (col.default !== undefined) {
|
|
400
|
-
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return colSql;
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// Primary Key with CONSTRAINT name
|
|
407
|
-
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
408
|
-
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
409
|
-
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
410
|
-
colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
protected dropTable(def: DropTableQueryDef): QueryBuildResult {
|
|
417
|
-
return { sql: `DROP TABLE ${this.tableName(def.table)}` };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
|
|
421
|
-
return {
|
|
422
|
-
sql: `ALTER TABLE ${this.tableName(def.table)} RENAME TO ${this.expr.wrap(def.newName)}`,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
427
|
-
// PostgreSQL: RESTART IDENTITY로 시퀀스 리셋
|
|
428
|
-
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)} RESTART IDENTITY` };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
//#endregion
|
|
432
|
-
|
|
433
|
-
//#region ========== DDL - Column ==========
|
|
434
|
-
|
|
435
|
-
protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
|
|
436
|
-
const table = this.tableName(def.table);
|
|
437
|
-
const col = def.column;
|
|
438
|
-
|
|
439
|
-
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
440
|
-
|
|
441
|
-
// nullable: true → NULL, else → NOT NULL
|
|
442
|
-
if (col.nullable === true) {
|
|
443
|
-
colSql += " NULL";
|
|
444
|
-
} else {
|
|
445
|
-
colSql += " NOT NULL";
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (col.autoIncrement) {
|
|
449
|
-
colSql += " GENERATED BY DEFAULT AS IDENTITY";
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (col.default !== undefined) {
|
|
453
|
-
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
|
|
460
|
-
return {
|
|
461
|
-
sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
|
|
466
|
-
const table = this.tableName(def.table);
|
|
467
|
-
const col = def.column;
|
|
468
|
-
|
|
469
|
-
// PostgreSQL: ALTER COLUMN은 여러 ALTER 필요
|
|
470
|
-
const parts: string[] = [];
|
|
471
|
-
|
|
472
|
-
// TYPE 변경
|
|
473
|
-
parts.push(
|
|
474
|
-
`ALTER COLUMN ${this.expr.wrap(col.name)} TYPE ${this.expr.renderDataType(col.dataType)}`,
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
// NULL 변경
|
|
478
|
-
if (col.nullable === false) {
|
|
479
|
-
parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} SET NOT NULL`);
|
|
480
|
-
} else {
|
|
481
|
-
parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} DROP NOT NULL`);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// DEFAULT 변경
|
|
485
|
-
if (col.default !== undefined) {
|
|
486
|
-
parts.push(
|
|
487
|
-
`ALTER COLUMN ${this.expr.wrap(col.name)} SET DEFAULT ${this.expr.escapeValue(col.default)}`,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return { sql: `ALTER TABLE ${table} ${parts.join(", ")}` };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
495
|
-
const table = this.tableName(def.table);
|
|
496
|
-
return {
|
|
497
|
-
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
//#endregion
|
|
502
|
-
|
|
503
|
-
//#region ========== DDL - Constraint ==========
|
|
504
|
-
|
|
505
|
-
protected addPk(def: AddPkQueryDef): QueryBuildResult {
|
|
506
|
-
const table = this.tableName(def.table);
|
|
507
|
-
const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
508
|
-
const pkName = `PK_${def.table.name}`;
|
|
509
|
-
return {
|
|
510
|
-
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
protected dropPk(def: DropPkQueryDef): QueryBuildResult {
|
|
515
|
-
const table = this.tableName(def.table);
|
|
516
|
-
const pkName = `PK_${def.table.name}`;
|
|
517
|
-
return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
protected addFk(def: AddFkQueryDef): QueryBuildResult {
|
|
521
|
-
const table = this.tableName(def.table);
|
|
522
|
-
const fk = def.foreignKey;
|
|
523
|
-
const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
524
|
-
const targetTable = this.tableName(fk.targetTable);
|
|
525
|
-
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
526
|
-
|
|
527
|
-
let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
|
|
528
|
-
|
|
529
|
-
// PostgreSQL: FK용
|
|
530
|
-
const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
|
|
531
|
-
sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
|
|
532
|
-
|
|
533
|
-
return { sql };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
protected dropFk(def: DropFkQueryDef): QueryBuildResult {
|
|
537
|
-
return {
|
|
538
|
-
sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
|
|
543
|
-
const table = this.tableName(def.table);
|
|
544
|
-
const idx = def.index;
|
|
545
|
-
const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
|
|
546
|
-
const unique = idx.unique ? "UNIQUE " : "";
|
|
547
|
-
return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
|
|
551
|
-
// PostgreSQL:
|
|
552
|
-
const schema = def.table.schema ?? "public";
|
|
553
|
-
return { sql: `DROP INDEX ${this.expr.wrap(schema)}.${this.expr.wrap(def.index)}` };
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
//#endregion
|
|
557
|
-
|
|
558
|
-
//#region ========== DDL - View/Procedure ==========
|
|
559
|
-
|
|
560
|
-
protected createView(def: CreateViewQueryDef): QueryBuildResult {
|
|
561
|
-
const view = this.tableName(def.view);
|
|
562
|
-
const selectSql = this.select(def.queryDef).sql;
|
|
563
|
-
return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
protected dropView(def: DropViewQueryDef): QueryBuildResult {
|
|
567
|
-
return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
571
|
-
const proc = this.tableName(def.procedure);
|
|
572
|
-
|
|
573
|
-
// params
|
|
574
|
-
const paramList =
|
|
575
|
-
def.params
|
|
576
|
-
?.map((p) => {
|
|
577
|
-
let sql = `${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
|
|
578
|
-
if (p.default !== undefined) {
|
|
579
|
-
sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
|
|
580
|
-
}
|
|
581
|
-
return sql;
|
|
582
|
-
})
|
|
583
|
-
.join(", ") ?? "";
|
|
584
|
-
|
|
585
|
-
// returns
|
|
586
|
-
let returnClause = "VOID";
|
|
587
|
-
if (def.returns && def.returns.length > 0) {
|
|
588
|
-
const returnFields = def.returns
|
|
589
|
-
.map((r) => `${this.expr.wrap(r.name)} ${this.expr.renderDataType(r.dataType)}`)
|
|
590
|
-
.join(", ");
|
|
591
|
-
returnClause = `TABLE(${returnFields})`;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
let sql = `CREATE OR REPLACE FUNCTION ${proc}(${paramList})\n`;
|
|
595
|
-
sql += `RETURNS ${returnClause} AS $$\n`;
|
|
596
|
-
sql += `BEGIN\n`;
|
|
597
|
-
sql += def.query;
|
|
598
|
-
if (!def.query.trim().endsWith(";")) {
|
|
599
|
-
sql += ";";
|
|
600
|
-
}
|
|
601
|
-
sql += `\nEND;\n`;
|
|
602
|
-
sql += `$$ LANGUAGE plpgsql`;
|
|
603
|
-
|
|
604
|
-
return { sql };
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
protected dropProc(def: DropProcQueryDef): QueryBuildResult {
|
|
608
|
-
return { sql: `DROP FUNCTION IF EXISTS ${this.tableName(def.procedure)}()` };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
protected execProc(def: ExecProcQueryDef): QueryBuildResult {
|
|
612
|
-
const proc = this.tableName(def.procedure);
|
|
613
|
-
if (def.params == null || Object.keys(def.params).length === 0) {
|
|
614
|
-
return { sql: `SELECT ${proc}()` };
|
|
615
|
-
}
|
|
616
|
-
const params = Object.values(def.params)
|
|
617
|
-
.map((p) => this.expr.render(p))
|
|
618
|
-
.join(", ");
|
|
619
|
-
return { sql: `SELECT ${proc}(${params})` };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
//#endregion
|
|
623
|
-
|
|
624
|
-
//#region ========== Utils ==========
|
|
625
|
-
|
|
626
|
-
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
627
|
-
const schemaName = def.schema ?? "public";
|
|
628
|
-
// SQL Injection 방어: 스키마명 유효성
|
|
629
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
}
|
|
632
|
-
const schema = this.expr.escapeString(schemaName);
|
|
633
|
-
return {
|
|
634
|
-
sql: `
|
|
635
|
-
DO $$
|
|
636
|
-
DECLARE
|
|
637
|
-
r RECORD;
|
|
638
|
-
BEGIN
|
|
639
|
-
-- FK
|
|
640
|
-
FOR r IN (SELECT conname, conrelid::regclass AS tablename
|
|
641
|
-
FROM pg_constraint
|
|
642
|
-
WHERE contype = 'f' AND connamespace = '${schema}'::regnamespace)
|
|
643
|
-
LOOP
|
|
644
|
-
EXECUTE 'ALTER TABLE ' || r.tablename || ' DROP CONSTRAINT ' || quote_ident(r.conname);
|
|
645
|
-
END LOOP;
|
|
646
|
-
|
|
647
|
-
--
|
|
648
|
-
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
|
|
649
|
-
LOOP
|
|
650
|
-
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
651
|
-
END LOOP;
|
|
652
|
-
|
|
653
|
-
--
|
|
654
|
-
FOR r IN (SELECT viewname FROM pg_views WHERE schemaname = '${schema}')
|
|
655
|
-
LOOP
|
|
656
|
-
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.viewname) || ' CASCADE';
|
|
657
|
-
END LOOP;
|
|
658
|
-
|
|
659
|
-
--
|
|
660
|
-
FOR r IN (SELECT proname, pg_get_function_identity_arguments(oid) AS args
|
|
661
|
-
FROM pg_proc WHERE pronamespace = '${schema}'::regnamespace)
|
|
662
|
-
LOOP
|
|
663
|
-
EXECUTE 'DROP FUNCTION IF EXISTS ' || quote_ident(r.proname) || '(' || r.args || ') CASCADE';
|
|
664
|
-
END LOOP;
|
|
665
|
-
END $$`,
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
670
|
-
const schemaName = def.schema ?? "public";
|
|
671
|
-
const schema = this.expr.escapeString(schemaName);
|
|
672
|
-
return { sql: `SELECT nspname FROM pg_namespace WHERE nspname = '${schema}'` };
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
676
|
-
const table = this.tableName(def.table);
|
|
677
|
-
if (def.switch === "on") {
|
|
678
|
-
// PostgreSQL:
|
|
679
|
-
return { sql: `ALTER TABLE ${table} ENABLE TRIGGER ALL` };
|
|
680
|
-
}
|
|
681
|
-
// PostgreSQL:
|
|
682
|
-
return { sql: `ALTER TABLE ${table} DISABLE TRIGGER ALL` };
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
//#endregion
|
|
686
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
AddColumnQueryDef,
|
|
3
|
+
AddFkQueryDef,
|
|
4
|
+
AddIdxQueryDef,
|
|
5
|
+
AddPkQueryDef,
|
|
6
|
+
ClearSchemaQueryDef,
|
|
7
|
+
CreateProcQueryDef,
|
|
8
|
+
CreateTableQueryDef,
|
|
9
|
+
CreateViewQueryDef,
|
|
10
|
+
SchemaExistsQueryDef,
|
|
11
|
+
DeleteQueryDef,
|
|
12
|
+
DropColumnQueryDef,
|
|
13
|
+
DropFkQueryDef,
|
|
14
|
+
DropIdxQueryDef,
|
|
15
|
+
DropPkQueryDef,
|
|
16
|
+
DropProcQueryDef,
|
|
17
|
+
DropTableQueryDef,
|
|
18
|
+
DropViewQueryDef,
|
|
19
|
+
ExecProcQueryDef,
|
|
20
|
+
InsertIfNotExistsQueryDef,
|
|
21
|
+
InsertIntoQueryDef,
|
|
22
|
+
InsertQueryDef,
|
|
23
|
+
ModifyColumnQueryDef,
|
|
24
|
+
QueryDefObjectName,
|
|
25
|
+
RenameColumnQueryDef,
|
|
26
|
+
RenameTableQueryDef,
|
|
27
|
+
SelectQueryDef,
|
|
28
|
+
SelectQueryDefJoin,
|
|
29
|
+
SwitchFkQueryDef,
|
|
30
|
+
TruncateQueryDef,
|
|
31
|
+
UpdateQueryDef,
|
|
32
|
+
UpsertQueryDef,
|
|
33
|
+
} from "../../types/query-def";
|
|
34
|
+
import type { QueryBuildResult } from "../../types/db";
|
|
35
|
+
import { QueryBuilderBase } from "../base/query-builder-base";
|
|
36
|
+
import { PostgresqlExprRenderer } from "./postgresql-expr-renderer";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* PostgreSQL QueryBuilder
|
|
40
|
+
*
|
|
41
|
+
* PostgreSQL 특이사항:
|
|
42
|
+
* - OUTPUT: RETURNING 절 사용 (네이티브 지원)
|
|
43
|
+
* - TRUNCATE: RESTART IDENTITY option 필요
|
|
44
|
+
* - UPSERT: CTE 방식 (INSERT ... ON CONFLICT는 단일 unique 제약만 지원)
|
|
45
|
+
* - AUTO_INCREMENT: GENERATED BY DEFAULT AS IDENTITY (explicit value 지정 가능)
|
|
46
|
+
* - FK Add 시 Index 별도 Generate 필요 (MySQL과 달리)
|
|
47
|
+
*/
|
|
48
|
+
export class PostgresqlQueryBuilder extends QueryBuilderBase {
|
|
49
|
+
protected expr = new PostgresqlExprRenderer((def) => this.select(def).sql);
|
|
50
|
+
|
|
51
|
+
//#region ========== 유틸리티 ==========
|
|
52
|
+
|
|
53
|
+
/** Table명 Render (PostgreSQL: database는 connection에서 processing, schema.table만 사용) */
|
|
54
|
+
protected tableName(obj: QueryDefObjectName): string {
|
|
55
|
+
const schema = obj.schema ?? "public";
|
|
56
|
+
return `${this.expr.wrap(schema)}.${this.expr.wrap(obj.name)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** LIMIT...OFFSET 절 Render */
|
|
60
|
+
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
61
|
+
if (limit != null) {
|
|
62
|
+
const [offset, count] = limit;
|
|
63
|
+
return ` LIMIT ${count} OFFSET ${offset}`;
|
|
64
|
+
}
|
|
65
|
+
if (top != null) {
|
|
66
|
+
return ` LIMIT ${top}`;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
72
|
+
const alias = this.expr.wrap(join.as);
|
|
73
|
+
|
|
74
|
+
// LATERAL JOIN 필요 여부 감지
|
|
75
|
+
if (this.needsLateral(join)) {
|
|
76
|
+
// from이 배열(UNION ALL)이면 renderFrom(join.from),
|
|
77
|
+
// 그 외(orderBy, top, select 등)면 renderFrom(join)으로 Subquery Generate
|
|
78
|
+
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
79
|
+
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 일반 JOIN
|
|
83
|
+
const from = this.renderFrom(join.from);
|
|
84
|
+
const where =
|
|
85
|
+
join.where != null && join.where.length > 0
|
|
86
|
+
? ` ON ${this.expr.renderWhere(join.where)}`
|
|
87
|
+
: " ON TRUE";
|
|
88
|
+
return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
|
|
93
|
+
//#region ========== DML - SELECT ==========
|
|
94
|
+
|
|
95
|
+
protected select(def: SelectQueryDef): QueryBuildResult {
|
|
96
|
+
// WITH (CTE)
|
|
97
|
+
let sql = "";
|
|
98
|
+
if (def.with != null) {
|
|
99
|
+
const { name, base, recursive } = def.with;
|
|
100
|
+
sql += `WITH RECURSIVE ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// SELECT
|
|
104
|
+
sql += "SELECT";
|
|
105
|
+
if (def.distinct) {
|
|
106
|
+
sql += " DISTINCT";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// columns
|
|
110
|
+
if (def.select != null) {
|
|
111
|
+
const cols = Object.entries(def.select).map(
|
|
112
|
+
([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
|
|
113
|
+
);
|
|
114
|
+
sql += ` ${cols.join(", ")}`;
|
|
115
|
+
} else {
|
|
116
|
+
sql += " *";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// FROM
|
|
120
|
+
if (def.from != null) {
|
|
121
|
+
const from = this.renderFrom(def.from);
|
|
122
|
+
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// JOINs
|
|
126
|
+
sql += this.renderJoins(def.joins);
|
|
127
|
+
|
|
128
|
+
// WHERE
|
|
129
|
+
sql += this.renderWhere(def.where);
|
|
130
|
+
|
|
131
|
+
// GROUP BY
|
|
132
|
+
sql += this.renderGroupBy(def.groupBy);
|
|
133
|
+
|
|
134
|
+
// HAVING
|
|
135
|
+
sql += this.renderHaving(def.having);
|
|
136
|
+
|
|
137
|
+
// ORDER BY
|
|
138
|
+
sql += this.renderOrderBy(def.orderBy);
|
|
139
|
+
|
|
140
|
+
// LIMIT
|
|
141
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
142
|
+
|
|
143
|
+
// LOCK (FOR UPDATE at end)
|
|
144
|
+
if (def.lock) {
|
|
145
|
+
sql += " FOR UPDATE";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { sql };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
|
|
153
|
+
//#region ========== DML - INSERT ==========
|
|
154
|
+
|
|
155
|
+
protected insert(def: InsertQueryDef): QueryBuildResult {
|
|
156
|
+
const table = this.tableName(def.table);
|
|
157
|
+
|
|
158
|
+
if (def.records.length === 0) {
|
|
159
|
+
throw new Error("INSERT requires at least one record.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const columns = Object.keys(def.records[0]);
|
|
163
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
164
|
+
|
|
165
|
+
const valuesList = def.records.map((record) => {
|
|
166
|
+
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
167
|
+
return `(${values.join(", ")})`;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
let sql = `INSERT INTO ${table} (${colList})`;
|
|
171
|
+
|
|
172
|
+
// GENERATED BY DEFAULT AS IDENTITY이므로 explicit value 삽입 시 Add 구문 불필요
|
|
173
|
+
// overrideIdentity 파라미터는 MSSQL(SET IDENTITY_INSERT) 호환성을 위해 유지되지만
|
|
174
|
+
// PostgreSQL에서는 GENERATED BY DEFAULT가 automatic으로 explicit 값을 허용함
|
|
175
|
+
// (참고: GENERATED ALWAYS였다면 OVERRIDING SYSTEM VALUE 필요)
|
|
176
|
+
|
|
177
|
+
sql += ` VALUES ${valuesList.join(", ")}`;
|
|
178
|
+
|
|
179
|
+
// RETURNING (PostgreSQL 네이티브 지원)
|
|
180
|
+
if (def.output != null) {
|
|
181
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
182
|
+
sql += ` RETURNING ${outputCols}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { sql };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
|
|
189
|
+
const table = this.tableName(def.table);
|
|
190
|
+
|
|
191
|
+
const columns = Object.keys(def.record);
|
|
192
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
193
|
+
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
194
|
+
|
|
195
|
+
// existsSelectQuery를 SELECT 1 AS _ 형태로 Render
|
|
196
|
+
const existsQuerySql = this.select({
|
|
197
|
+
...def.existsSelectQuery,
|
|
198
|
+
select: { _: { type: "value", value: 1 } },
|
|
199
|
+
}).sql;
|
|
200
|
+
|
|
201
|
+
let sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
202
|
+
|
|
203
|
+
// RETURNING
|
|
204
|
+
if (def.output != null) {
|
|
205
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
206
|
+
sql += ` RETURNING ${outputCols}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { sql };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
|
|
213
|
+
const table = this.tableName(def.table);
|
|
214
|
+
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
215
|
+
|
|
216
|
+
// INSERT INTO SELECT에서 columns 추출
|
|
217
|
+
const selectDef = def.recordsSelectQuery;
|
|
218
|
+
const colList =
|
|
219
|
+
selectDef.select != null
|
|
220
|
+
? Object.keys(selectDef.select)
|
|
221
|
+
.map((c) => this.expr.wrap(c))
|
|
222
|
+
.join(", ")
|
|
223
|
+
: "*";
|
|
224
|
+
|
|
225
|
+
let sql = `INSERT INTO ${table} (${colList}) ${selectSql}`;
|
|
226
|
+
|
|
227
|
+
// RETURNING
|
|
228
|
+
if (def.output != null) {
|
|
229
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
230
|
+
sql += ` RETURNING ${outputCols}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { sql };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
|
|
238
|
+
//#region ========== DML - UPDATE ==========
|
|
239
|
+
|
|
240
|
+
protected update(def: UpdateQueryDef): QueryBuildResult {
|
|
241
|
+
const table = this.tableName(def.table);
|
|
242
|
+
const alias = this.expr.wrap(def.as);
|
|
243
|
+
|
|
244
|
+
// SET
|
|
245
|
+
const setParts = Object.entries(def.record).map(
|
|
246
|
+
([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
let sql = `UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
|
|
250
|
+
|
|
251
|
+
// PostgreSQL: JOIN은 FROM 절로 processing
|
|
252
|
+
if (def.joins != null && def.joins.length > 0) {
|
|
253
|
+
const joinTables = def.joins.map((j) => {
|
|
254
|
+
const from = this.renderFrom(j.from);
|
|
255
|
+
return `${from} AS ${this.expr.wrap(j.as)}`;
|
|
256
|
+
});
|
|
257
|
+
sql += ` FROM ${joinTables.join(", ")}`;
|
|
258
|
+
|
|
259
|
+
// JOIN ON 조건을 WHERE에 Add
|
|
260
|
+
const joinConditions = def.joins
|
|
261
|
+
.filter((j) => j.where != null && j.where.length > 0)
|
|
262
|
+
.map((j) => this.expr.renderWhere(j.where!));
|
|
263
|
+
if (joinConditions.length > 0) {
|
|
264
|
+
const whereCondition =
|
|
265
|
+
def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
|
|
266
|
+
const allConditions =
|
|
267
|
+
whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
|
|
268
|
+
sql += ` WHERE ${allConditions.join(" AND ")}`;
|
|
269
|
+
} else {
|
|
270
|
+
sql += this.renderWhere(def.where);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
sql += this.renderWhere(def.where);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// RETURNING
|
|
277
|
+
if (def.output != null) {
|
|
278
|
+
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
279
|
+
sql += ` RETURNING ${outputCols}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { sql };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
//#endregion
|
|
286
|
+
|
|
287
|
+
//#region ========== DML - DELETE ==========
|
|
288
|
+
|
|
289
|
+
protected delete(def: DeleteQueryDef): QueryBuildResult {
|
|
290
|
+
const table = this.tableName(def.table);
|
|
291
|
+
const alias = this.expr.wrap(def.as);
|
|
292
|
+
|
|
293
|
+
let sql = `DELETE FROM ${table} AS ${alias}`;
|
|
294
|
+
|
|
295
|
+
// PostgreSQL: JOIN은 USING 절로 processing
|
|
296
|
+
if (def.joins != null && def.joins.length > 0) {
|
|
297
|
+
const joinTables = def.joins.map((j) => {
|
|
298
|
+
const from = this.renderFrom(j.from);
|
|
299
|
+
return `${from} AS ${this.expr.wrap(j.as)}`;
|
|
300
|
+
});
|
|
301
|
+
sql += ` USING ${joinTables.join(", ")}`;
|
|
302
|
+
|
|
303
|
+
// JOIN ON 조건을 WHERE에 Add
|
|
304
|
+
const joinConditions = def.joins
|
|
305
|
+
.filter((j) => j.where != null && j.where.length > 0)
|
|
306
|
+
.map((j) => this.expr.renderWhere(j.where!));
|
|
307
|
+
if (joinConditions.length > 0) {
|
|
308
|
+
const whereCondition =
|
|
309
|
+
def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
|
|
310
|
+
const allConditions =
|
|
311
|
+
whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
|
|
312
|
+
sql += ` WHERE ${allConditions.join(" AND ")}`;
|
|
313
|
+
} else {
|
|
314
|
+
sql += this.renderWhere(def.where);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
sql += this.renderWhere(def.where);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// RETURNING (PostgreSQL: DELETE에서도 지원)
|
|
321
|
+
if (def.output != null) {
|
|
322
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
323
|
+
sql += ` RETURNING ${outputCols}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { sql };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//#endregion
|
|
330
|
+
|
|
331
|
+
//#region ========== DML - UPSERT ==========
|
|
332
|
+
|
|
333
|
+
protected upsert(def: UpsertQueryDef): QueryBuildResult {
|
|
334
|
+
// PostgreSQL: CTE 방식 (ON CONFLICT는 단일 unique 제약만 지원하므로 범용성 위해 CTE 사용)
|
|
335
|
+
const table = this.tableName(def.table);
|
|
336
|
+
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
337
|
+
|
|
338
|
+
// UPDATE SET part
|
|
339
|
+
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
340
|
+
([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// INSERT part
|
|
344
|
+
const insertColumns = Object.keys(def.insertRecord);
|
|
345
|
+
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
346
|
+
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
347
|
+
|
|
348
|
+
// WHERE condition
|
|
349
|
+
const whereCondition =
|
|
350
|
+
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
351
|
+
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
352
|
+
: "TRUE";
|
|
353
|
+
|
|
354
|
+
// OUTPUT column
|
|
355
|
+
const outputCols =
|
|
356
|
+
def.output != null ? def.output.columns.map((c) => this.expr.wrap(c)).join(", ") : "*";
|
|
357
|
+
|
|
358
|
+
// CTE 방식 UPSERT
|
|
359
|
+
let sql = `WITH matched AS (\n`;
|
|
360
|
+
sql += ` SELECT ${alias}.* FROM ${table} AS ${alias} WHERE ${whereCondition}\n`;
|
|
361
|
+
sql += `),\n`;
|
|
362
|
+
sql += `updated AS (\n`;
|
|
363
|
+
sql += ` UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")}\n`;
|
|
364
|
+
sql += ` WHERE ${whereCondition}\n`;
|
|
365
|
+
sql += ` RETURNING ${outputCols}\n`;
|
|
366
|
+
sql += `),\n`;
|
|
367
|
+
sql += `inserted AS (\n`;
|
|
368
|
+
sql += ` INSERT INTO ${table} (${insertColList})\n`;
|
|
369
|
+
sql += ` SELECT ${insertValues}\n`;
|
|
370
|
+
sql += ` WHERE NOT EXISTS (SELECT 1 FROM matched)\n`;
|
|
371
|
+
sql += ` RETURNING ${outputCols}\n`;
|
|
372
|
+
sql += `)\n`;
|
|
373
|
+
sql += `SELECT * FROM updated UNION ALL SELECT * FROM inserted`;
|
|
374
|
+
|
|
375
|
+
return { sql };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
//#endregion
|
|
379
|
+
|
|
380
|
+
//#region ========== DDL - Table ==========
|
|
381
|
+
|
|
382
|
+
protected createTable(def: CreateTableQueryDef): QueryBuildResult {
|
|
383
|
+
const table = this.tableName(def.table);
|
|
384
|
+
|
|
385
|
+
const colDefs = def.columns.map((col) => {
|
|
386
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
387
|
+
|
|
388
|
+
// nullable: true → NULL, else → NOT NULL
|
|
389
|
+
if (col.nullable === true) {
|
|
390
|
+
colSql += " NULL";
|
|
391
|
+
} else {
|
|
392
|
+
colSql += " NOT NULL";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (col.autoIncrement) {
|
|
396
|
+
colSql += " GENERATED BY DEFAULT AS IDENTITY";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (col.default !== undefined) {
|
|
400
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return colSql;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Primary Key with CONSTRAINT name
|
|
407
|
+
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
408
|
+
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
409
|
+
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
410
|
+
colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
protected dropTable(def: DropTableQueryDef): QueryBuildResult {
|
|
417
|
+
return { sql: `DROP TABLE ${this.tableName(def.table)}` };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
|
|
421
|
+
return {
|
|
422
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} RENAME TO ${this.expr.wrap(def.newName)}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
427
|
+
// PostgreSQL: RESTART IDENTITY로 시퀀스 리셋
|
|
428
|
+
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)} RESTART IDENTITY` };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
|
|
433
|
+
//#region ========== DDL - Column ==========
|
|
434
|
+
|
|
435
|
+
protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
|
|
436
|
+
const table = this.tableName(def.table);
|
|
437
|
+
const col = def.column;
|
|
438
|
+
|
|
439
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
440
|
+
|
|
441
|
+
// nullable: true → NULL, else → NOT NULL
|
|
442
|
+
if (col.nullable === true) {
|
|
443
|
+
colSql += " NULL";
|
|
444
|
+
} else {
|
|
445
|
+
colSql += " NOT NULL";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (col.autoIncrement) {
|
|
449
|
+
colSql += " GENERATED BY DEFAULT AS IDENTITY";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (col.default !== undefined) {
|
|
453
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
|
|
460
|
+
return {
|
|
461
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
|
|
466
|
+
const table = this.tableName(def.table);
|
|
467
|
+
const col = def.column;
|
|
468
|
+
|
|
469
|
+
// PostgreSQL: ALTER COLUMN은 여러 ALTER 필요
|
|
470
|
+
const parts: string[] = [];
|
|
471
|
+
|
|
472
|
+
// TYPE 변경
|
|
473
|
+
parts.push(
|
|
474
|
+
`ALTER COLUMN ${this.expr.wrap(col.name)} TYPE ${this.expr.renderDataType(col.dataType)}`,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// NULL 변경
|
|
478
|
+
if (col.nullable === false) {
|
|
479
|
+
parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} SET NOT NULL`);
|
|
480
|
+
} else {
|
|
481
|
+
parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} DROP NOT NULL`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// DEFAULT 변경
|
|
485
|
+
if (col.default !== undefined) {
|
|
486
|
+
parts.push(
|
|
487
|
+
`ALTER COLUMN ${this.expr.wrap(col.name)} SET DEFAULT ${this.expr.escapeValue(col.default)}`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return { sql: `ALTER TABLE ${table} ${parts.join(", ")}` };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
495
|
+
const table = this.tableName(def.table);
|
|
496
|
+
return {
|
|
497
|
+
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
//#endregion
|
|
502
|
+
|
|
503
|
+
//#region ========== DDL - Constraint ==========
|
|
504
|
+
|
|
505
|
+
protected addPk(def: AddPkQueryDef): QueryBuildResult {
|
|
506
|
+
const table = this.tableName(def.table);
|
|
507
|
+
const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
508
|
+
const pkName = `PK_${def.table.name}`;
|
|
509
|
+
return {
|
|
510
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
protected dropPk(def: DropPkQueryDef): QueryBuildResult {
|
|
515
|
+
const table = this.tableName(def.table);
|
|
516
|
+
const pkName = `PK_${def.table.name}`;
|
|
517
|
+
return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
protected addFk(def: AddFkQueryDef): QueryBuildResult {
|
|
521
|
+
const table = this.tableName(def.table);
|
|
522
|
+
const fk = def.foreignKey;
|
|
523
|
+
const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
524
|
+
const targetTable = this.tableName(fk.targetTable);
|
|
525
|
+
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
526
|
+
|
|
527
|
+
let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
|
|
528
|
+
|
|
529
|
+
// PostgreSQL: FK용 Index 별도 Generate 필요
|
|
530
|
+
const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
|
|
531
|
+
sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
|
|
532
|
+
|
|
533
|
+
return { sql };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
protected dropFk(def: DropFkQueryDef): QueryBuildResult {
|
|
537
|
+
return {
|
|
538
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
|
|
543
|
+
const table = this.tableName(def.table);
|
|
544
|
+
const idx = def.index;
|
|
545
|
+
const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
|
|
546
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
547
|
+
return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
|
|
551
|
+
// PostgreSQL: Index는 schema 레벨에서 유니크하므로 Table명 불필요하지만 스키마는 명시 필요
|
|
552
|
+
const schema = def.table.schema ?? "public";
|
|
553
|
+
return { sql: `DROP INDEX ${this.expr.wrap(schema)}.${this.expr.wrap(def.index)}` };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
//#endregion
|
|
557
|
+
|
|
558
|
+
//#region ========== DDL - View/Procedure ==========
|
|
559
|
+
|
|
560
|
+
protected createView(def: CreateViewQueryDef): QueryBuildResult {
|
|
561
|
+
const view = this.tableName(def.view);
|
|
562
|
+
const selectSql = this.select(def.queryDef).sql;
|
|
563
|
+
return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
protected dropView(def: DropViewQueryDef): QueryBuildResult {
|
|
567
|
+
return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
571
|
+
const proc = this.tableName(def.procedure);
|
|
572
|
+
|
|
573
|
+
// params processing
|
|
574
|
+
const paramList =
|
|
575
|
+
def.params
|
|
576
|
+
?.map((p) => {
|
|
577
|
+
let sql = `${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
|
|
578
|
+
if (p.default !== undefined) {
|
|
579
|
+
sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
|
|
580
|
+
}
|
|
581
|
+
return sql;
|
|
582
|
+
})
|
|
583
|
+
.join(", ") ?? "";
|
|
584
|
+
|
|
585
|
+
// returns processing
|
|
586
|
+
let returnClause = "VOID";
|
|
587
|
+
if (def.returns && def.returns.length > 0) {
|
|
588
|
+
const returnFields = def.returns
|
|
589
|
+
.map((r) => `${this.expr.wrap(r.name)} ${this.expr.renderDataType(r.dataType)}`)
|
|
590
|
+
.join(", ");
|
|
591
|
+
returnClause = `TABLE(${returnFields})`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let sql = `CREATE OR REPLACE FUNCTION ${proc}(${paramList})\n`;
|
|
595
|
+
sql += `RETURNS ${returnClause} AS $$\n`;
|
|
596
|
+
sql += `BEGIN\n`;
|
|
597
|
+
sql += def.query;
|
|
598
|
+
if (!def.query.trim().endsWith(";")) {
|
|
599
|
+
sql += ";";
|
|
600
|
+
}
|
|
601
|
+
sql += `\nEND;\n`;
|
|
602
|
+
sql += `$$ LANGUAGE plpgsql`;
|
|
603
|
+
|
|
604
|
+
return { sql };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
protected dropProc(def: DropProcQueryDef): QueryBuildResult {
|
|
608
|
+
return { sql: `DROP FUNCTION IF EXISTS ${this.tableName(def.procedure)}()` };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
protected execProc(def: ExecProcQueryDef): QueryBuildResult {
|
|
612
|
+
const proc = this.tableName(def.procedure);
|
|
613
|
+
if (def.params == null || Object.keys(def.params).length === 0) {
|
|
614
|
+
return { sql: `SELECT ${proc}()` };
|
|
615
|
+
}
|
|
616
|
+
const params = Object.values(def.params)
|
|
617
|
+
.map((p) => this.expr.render(p))
|
|
618
|
+
.join(", ");
|
|
619
|
+
return { sql: `SELECT ${proc}(${params})` };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
|
|
624
|
+
//#region ========== Utils ==========
|
|
625
|
+
|
|
626
|
+
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
627
|
+
const schemaName = def.schema ?? "public";
|
|
628
|
+
// SQL Injection 방어: 스키마명 유효성 Validation
|
|
629
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
|
|
630
|
+
throw new Error(`Invalid schema name: ${schemaName}`);
|
|
631
|
+
}
|
|
632
|
+
const schema = this.expr.escapeString(schemaName);
|
|
633
|
+
return {
|
|
634
|
+
sql: `
|
|
635
|
+
DO $$
|
|
636
|
+
DECLARE
|
|
637
|
+
r RECORD;
|
|
638
|
+
BEGIN
|
|
639
|
+
-- FK constraint Delete
|
|
640
|
+
FOR r IN (SELECT conname, conrelid::regclass AS tablename
|
|
641
|
+
FROM pg_constraint
|
|
642
|
+
WHERE contype = 'f' AND connamespace = '${schema}'::regnamespace)
|
|
643
|
+
LOOP
|
|
644
|
+
EXECUTE 'ALTER TABLE ' || r.tablename || ' DROP CONSTRAINT ' || quote_ident(r.conname);
|
|
645
|
+
END LOOP;
|
|
646
|
+
|
|
647
|
+
-- Drop table
|
|
648
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
|
|
649
|
+
LOOP
|
|
650
|
+
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
651
|
+
END LOOP;
|
|
652
|
+
|
|
653
|
+
-- Drop view
|
|
654
|
+
FOR r IN (SELECT viewname FROM pg_views WHERE schemaname = '${schema}')
|
|
655
|
+
LOOP
|
|
656
|
+
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.viewname) || ' CASCADE';
|
|
657
|
+
END LOOP;
|
|
658
|
+
|
|
659
|
+
-- function Delete
|
|
660
|
+
FOR r IN (SELECT proname, pg_get_function_identity_arguments(oid) AS args
|
|
661
|
+
FROM pg_proc WHERE pronamespace = '${schema}'::regnamespace)
|
|
662
|
+
LOOP
|
|
663
|
+
EXECUTE 'DROP FUNCTION IF EXISTS ' || quote_ident(r.proname) || '(' || r.args || ') CASCADE';
|
|
664
|
+
END LOOP;
|
|
665
|
+
END $$`,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
670
|
+
const schemaName = def.schema ?? "public";
|
|
671
|
+
const schema = this.expr.escapeString(schemaName);
|
|
672
|
+
return { sql: `SELECT nspname FROM pg_namespace WHERE nspname = '${schema}'` };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
676
|
+
const table = this.tableName(def.table);
|
|
677
|
+
if (def.switch === "on") {
|
|
678
|
+
// PostgreSQL: Table의 모든 FK 트리거 Enable
|
|
679
|
+
return { sql: `ALTER TABLE ${table} ENABLE TRIGGER ALL` };
|
|
680
|
+
}
|
|
681
|
+
// PostgreSQL: Table의 모든 FK 트리거 Disable
|
|
682
|
+
return { sql: `ALTER TABLE ${table} DISABLE TRIGGER ALL` };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
//#endregion
|
|
686
|
+
}
|