@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.
Files changed (204) hide show
  1. package/README.md +54 -1447
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +9 -9
  4. package/dist/create-db-context.js.map +1 -1
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/initialize.d.ts +17 -17
  7. package/dist/ddl/initialize.js +2 -2
  8. package/dist/ddl/initialize.js.map +1 -1
  9. package/dist/ddl/relation-ddl.d.ts +6 -6
  10. package/dist/ddl/schema-ddl.d.ts +4 -4
  11. package/dist/ddl/table-ddl.d.ts +24 -24
  12. package/dist/ddl/table-ddl.js +4 -4
  13. package/dist/ddl/table-ddl.js.map +1 -1
  14. package/dist/errors/db-transaction-error.d.ts +15 -15
  15. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  16. package/dist/exec/executable.d.ts +23 -23
  17. package/dist/exec/executable.js +3 -3
  18. package/dist/exec/executable.js.map +1 -1
  19. package/dist/exec/queryable.d.ts +160 -160
  20. package/dist/exec/queryable.js +119 -119
  21. package/dist/exec/queryable.js.map +1 -1
  22. package/dist/exec/search-parser.d.ts +37 -37
  23. package/dist/exec/search-parser.d.ts.map +1 -1
  24. package/dist/expr/expr-unit.d.ts +4 -4
  25. package/dist/expr/expr.d.ts +257 -257
  26. package/dist/expr/expr.js +265 -265
  27. package/dist/expr/expr.js.map +1 -1
  28. package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
  29. package/dist/query-builder/base/expr-renderer-base.js +2 -2
  30. package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
  31. package/dist/query-builder/base/query-builder-base.d.ts +26 -26
  32. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  33. package/dist/query-builder/base/query-builder-base.js +22 -22
  34. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  35. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  36. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  37. package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
  38. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  39. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  40. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  41. package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
  42. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  43. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  44. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  45. package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
  46. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  47. package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
  48. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  49. package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
  50. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  51. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  52. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  53. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
  54. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  55. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
  56. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  57. package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
  58. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  59. package/dist/query-builder/query-builder.d.ts +1 -1
  60. package/dist/schema/factory/column-builder.d.ts +79 -79
  61. package/dist/schema/factory/column-builder.js +42 -42
  62. package/dist/schema/factory/index-builder.d.ts +39 -39
  63. package/dist/schema/factory/index-builder.js +26 -26
  64. package/dist/schema/factory/relation-builder.d.ts +99 -99
  65. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  66. package/dist/schema/factory/relation-builder.js +38 -38
  67. package/dist/schema/procedure-builder.d.ts +49 -49
  68. package/dist/schema/procedure-builder.d.ts.map +1 -1
  69. package/dist/schema/procedure-builder.js +33 -33
  70. package/dist/schema/table-builder.d.ts +59 -59
  71. package/dist/schema/table-builder.d.ts.map +1 -1
  72. package/dist/schema/table-builder.js +43 -43
  73. package/dist/schema/view-builder.d.ts +49 -49
  74. package/dist/schema/view-builder.d.ts.map +1 -1
  75. package/dist/schema/view-builder.js +32 -32
  76. package/dist/types/column.d.ts +22 -22
  77. package/dist/types/column.js +1 -1
  78. package/dist/types/column.js.map +1 -1
  79. package/dist/types/db.d.ts +40 -40
  80. package/dist/types/expr.d.ts +59 -59
  81. package/dist/types/expr.d.ts.map +1 -1
  82. package/dist/types/query-def.d.ts +44 -44
  83. package/dist/types/query-def.d.ts.map +1 -1
  84. package/dist/utils/result-parser.d.ts +11 -11
  85. package/dist/utils/result-parser.js +3 -3
  86. package/dist/utils/result-parser.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/create-db-context.ts +20 -20
  89. package/src/ddl/column-ddl.ts +4 -4
  90. package/src/ddl/initialize.ts +259 -259
  91. package/src/ddl/relation-ddl.ts +89 -89
  92. package/src/ddl/schema-ddl.ts +4 -4
  93. package/src/ddl/table-ddl.ts +189 -189
  94. package/src/errors/db-transaction-error.ts +13 -13
  95. package/src/exec/executable.ts +25 -25
  96. package/src/exec/queryable.ts +2033 -2033
  97. package/src/exec/search-parser.ts +57 -57
  98. package/src/expr/expr-unit.ts +4 -4
  99. package/src/expr/expr.ts +2140 -2140
  100. package/src/query-builder/base/expr-renderer-base.ts +237 -237
  101. package/src/query-builder/base/query-builder-base.ts +213 -213
  102. package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
  103. package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
  104. package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
  105. package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
  106. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
  107. package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
  108. package/src/query-builder/query-builder.ts +19 -19
  109. package/src/schema/factory/column-builder.ts +423 -423
  110. package/src/schema/factory/index-builder.ts +164 -164
  111. package/src/schema/factory/relation-builder.ts +453 -453
  112. package/src/schema/procedure-builder.ts +232 -232
  113. package/src/schema/table-builder.ts +319 -319
  114. package/src/schema/view-builder.ts +221 -221
  115. package/src/types/column.ts +188 -188
  116. package/src/types/db.ts +208 -208
  117. package/src/types/expr.ts +697 -697
  118. package/src/types/query-def.ts +513 -513
  119. package/src/utils/result-parser.ts +458 -458
  120. package/tests/db-context/create-db-context.spec.ts +224 -0
  121. package/tests/db-context/define-db-context.spec.ts +68 -0
  122. package/tests/ddl/basic.expected.ts +341 -0
  123. package/tests/ddl/basic.spec.ts +714 -0
  124. package/tests/ddl/column-builder.expected.ts +310 -0
  125. package/tests/ddl/column-builder.spec.ts +637 -0
  126. package/tests/ddl/index-builder.expected.ts +38 -0
  127. package/tests/ddl/index-builder.spec.ts +202 -0
  128. package/tests/ddl/procedure-builder.expected.ts +52 -0
  129. package/tests/ddl/procedure-builder.spec.ts +234 -0
  130. package/tests/ddl/relation-builder.expected.ts +36 -0
  131. package/tests/ddl/relation-builder.spec.ts +372 -0
  132. package/tests/ddl/table-builder.expected.ts +113 -0
  133. package/tests/ddl/table-builder.spec.ts +433 -0
  134. package/tests/ddl/view-builder.expected.ts +38 -0
  135. package/tests/ddl/view-builder.spec.ts +176 -0
  136. package/tests/dml/delete.expected.ts +96 -0
  137. package/tests/dml/delete.spec.ts +160 -0
  138. package/tests/dml/insert.expected.ts +192 -0
  139. package/tests/dml/insert.spec.ts +288 -0
  140. package/tests/dml/update.expected.ts +176 -0
  141. package/tests/dml/update.spec.ts +318 -0
  142. package/tests/dml/upsert.expected.ts +215 -0
  143. package/tests/dml/upsert.spec.ts +242 -0
  144. package/tests/errors/queryable-errors.spec.ts +177 -0
  145. package/tests/escape.spec.ts +100 -0
  146. package/tests/examples/pivot.expected.ts +211 -0
  147. package/tests/examples/pivot.spec.ts +533 -0
  148. package/tests/examples/sampling.expected.ts +69 -0
  149. package/tests/examples/sampling.spec.ts +104 -0
  150. package/tests/examples/unpivot.expected.ts +120 -0
  151. package/tests/examples/unpivot.spec.ts +226 -0
  152. package/tests/exec/search-parser.spec.ts +283 -0
  153. package/tests/executable/basic.expected.ts +18 -0
  154. package/tests/executable/basic.spec.ts +54 -0
  155. package/tests/expr/comparison.expected.ts +282 -0
  156. package/tests/expr/comparison.spec.ts +400 -0
  157. package/tests/expr/conditional.expected.ts +134 -0
  158. package/tests/expr/conditional.spec.ts +276 -0
  159. package/tests/expr/date.expected.ts +332 -0
  160. package/tests/expr/date.spec.ts +526 -0
  161. package/tests/expr/math.expected.ts +62 -0
  162. package/tests/expr/math.spec.ts +106 -0
  163. package/tests/expr/string.expected.ts +218 -0
  164. package/tests/expr/string.spec.ts +356 -0
  165. package/tests/expr/utility.expected.ts +147 -0
  166. package/tests/expr/utility.spec.ts +182 -0
  167. package/tests/select/basic.expected.ts +322 -0
  168. package/tests/select/basic.spec.ts +502 -0
  169. package/tests/select/filter.expected.ts +357 -0
  170. package/tests/select/filter.spec.ts +1068 -0
  171. package/tests/select/group.expected.ts +169 -0
  172. package/tests/select/group.spec.ts +244 -0
  173. package/tests/select/join.expected.ts +582 -0
  174. package/tests/select/join.spec.ts +805 -0
  175. package/tests/select/order.expected.ts +150 -0
  176. package/tests/select/order.spec.ts +189 -0
  177. package/tests/select/recursive-cte.expected.ts +244 -0
  178. package/tests/select/recursive-cte.spec.ts +514 -0
  179. package/tests/select/result-meta.spec.ts +270 -0
  180. package/tests/select/subquery.expected.ts +363 -0
  181. package/tests/select/subquery.spec.ts +537 -0
  182. package/tests/select/view.expected.ts +155 -0
  183. package/tests/select/view.spec.ts +235 -0
  184. package/tests/select/window.expected.ts +345 -0
  185. package/tests/select/window.spec.ts +618 -0
  186. package/tests/setup/MockExecutor.ts +18 -0
  187. package/tests/setup/TestDbContext.ts +59 -0
  188. package/tests/setup/models/Company.ts +13 -0
  189. package/tests/setup/models/Employee.ts +10 -0
  190. package/tests/setup/models/MonthlySales.ts +11 -0
  191. package/tests/setup/models/Post.ts +16 -0
  192. package/tests/setup/models/Sales.ts +10 -0
  193. package/tests/setup/models/User.ts +19 -0
  194. package/tests/setup/procedure/GetAllUsers.ts +9 -0
  195. package/tests/setup/procedure/GetUserById.ts +12 -0
  196. package/tests/setup/test-utils.ts +72 -0
  197. package/tests/setup/views/ActiveUsers.ts +8 -0
  198. package/tests/setup/views/UserSummary.ts +11 -0
  199. package/tests/types/nullable-queryable-record.spec.ts +145 -0
  200. package/tests/utils/result-parser-perf.spec.ts +210 -0
  201. package/tests/utils/result-parser.spec.ts +701 -0
  202. package/docs/expressions.md +0 -172
  203. package/docs/queries.md +0 -444
  204. 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 추가인덱스 별도 생성 필요 (MySQL과 달리)
47
- */
48
- export class PostgresqlQueryBuilder extends QueryBuilderBase {
49
- protected expr = new PostgresqlExprRenderer((def) => this.select(def).sql);
50
-
51
- //#region ========== 유틸리티 ==========
52
-
53
- /** 테이블명 렌더링 (PostgreSQL: database는 connection에서 처리, 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 절 렌더링 */
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(`유효하지 않은 스키마명: ${schemaName}`);
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: 테이블의 모든 FK 트리거 활성화
679
- return { sql: `ALTER TABLE ${table} ENABLE TRIGGER ALL` };
680
- }
681
- // PostgreSQL: 테이블의 모든 FK 트리거 비활성화
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 AddIndex 별도 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
+ }