@simplysm/orm-common 14.0.16 → 14.0.18

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 (75) hide show
  1. package/README.md +25 -19
  2. package/dist/db-context.d.ts +133 -0
  3. package/dist/db-context.d.ts.map +1 -0
  4. package/dist/db-context.js +325 -0
  5. package/dist/db-context.js.map +1 -0
  6. package/dist/ddl/initialize.d.ts +5 -3
  7. package/dist/ddl/initialize.d.ts.map +1 -1
  8. package/dist/ddl/initialize.js +39 -32
  9. package/dist/ddl/initialize.js.map +1 -1
  10. package/dist/exec/executable.js +1 -1
  11. package/dist/exec/executable.js.map +1 -1
  12. package/dist/exec/queryable.js +2 -2
  13. package/dist/exec/queryable.js.map +1 -1
  14. package/dist/exec/search-parser.d.ts.map +1 -1
  15. package/dist/exec/search-parser.js +2 -1
  16. package/dist/exec/search-parser.js.map +1 -1
  17. package/dist/expr/expr.d.ts +2 -2
  18. package/dist/expr/expr.js +2 -2
  19. package/dist/index.d.ts +1 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/query-builder/mssql/mssql-expr-renderer.js +1 -1
  24. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  25. package/dist/query-builder/mysql/mysql-expr-renderer.js +1 -1
  26. package/dist/query-builder/mysql/mysql-query-builder.js +1 -1
  27. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  28. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +7 -0
  29. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  30. package/dist/query-builder/postgresql/postgresql-query-builder.js +86 -16
  31. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  32. package/dist/schema/factory/relation-builder.d.ts +44 -67
  33. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  34. package/dist/schema/factory/relation-builder.js +37 -78
  35. package/dist/schema/factory/relation-builder.js.map +1 -1
  36. package/dist/schema/view-builder.d.ts +1 -1
  37. package/dist/schema/view-builder.d.ts.map +1 -1
  38. package/dist/schema/view-builder.js +0 -2
  39. package/dist/schema/view-builder.js.map +1 -1
  40. package/dist/types/column.js +1 -1
  41. package/dist/types/column.js.map +1 -1
  42. package/dist/types/db-context-def.d.ts +2 -42
  43. package/dist/types/db-context-def.d.ts.map +1 -1
  44. package/dist/utils/result-parser.js +31 -23
  45. package/dist/utils/result-parser.js.map +1 -1
  46. package/docs/core.md +110 -50
  47. package/docs/schema-builders.md +13 -19
  48. package/docs/types.md +2 -41
  49. package/package.json +3 -3
  50. package/src/db-context.ts +455 -0
  51. package/src/ddl/initialize.ts +49 -37
  52. package/src/exec/executable.ts +1 -1
  53. package/src/exec/queryable.ts +2 -2
  54. package/src/exec/search-parser.ts +2 -1
  55. package/src/expr/expr.ts +2 -2
  56. package/src/index.ts +2 -3
  57. package/src/query-builder/mssql/mssql-expr-renderer.ts +1 -1
  58. package/src/query-builder/mysql/mysql-expr-renderer.ts +1 -1
  59. package/src/query-builder/mysql/mysql-query-builder.ts +1 -1
  60. package/src/query-builder/postgresql/postgresql-query-builder.ts +93 -14
  61. package/src/schema/factory/relation-builder.ts +56 -87
  62. package/src/schema/view-builder.ts +1 -3
  63. package/src/types/column.ts +1 -1
  64. package/src/types/db-context-def.ts +2 -60
  65. package/src/utils/result-parser.ts +29 -22
  66. package/dist/create-db-context.d.ts +0 -34
  67. package/dist/create-db-context.d.ts.map +0 -1
  68. package/dist/create-db-context.js +0 -329
  69. package/dist/create-db-context.js.map +0 -1
  70. package/dist/define-db-context.d.ts +0 -15
  71. package/dist/define-db-context.d.ts.map +0 -1
  72. package/dist/define-db-context.js +0 -12
  73. package/dist/define-db-context.js.map +0 -1
  74. package/src/create-db-context.ts +0 -409
  75. package/src/define-db-context.ts +0 -28
@@ -522,7 +522,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
522
522
  }
523
523
 
524
524
  protected random(_expr: ExprRandom): string {
525
- return "NEWID()";
525
+ return "(ABS(CHECKSUM(NEWID())) / 2147483647.0)";
526
526
  }
527
527
 
528
528
  protected cast(expr: ExprCast): string {
@@ -376,7 +376,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
376
376
  }
377
377
 
378
378
  protected isoWeek(expr: ExprIsoWeek): string {
379
- return `WEEK(${this.render(expr.arg)}, 1)`;
379
+ return `WEEK(${this.render(expr.arg)}, 3)`;
380
380
  }
381
381
 
382
382
  protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
@@ -731,7 +731,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
731
731
  sql: `
732
732
  SET FOREIGN_KEY_CHECKS = 0;
733
733
  SET @tables = NULL;
734
- SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
734
+ SELECT GROUP_CONCAT(CONCAT('\`', REPLACE(table_name, '\`', '\`\`'), '\`')) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
735
735
  SET @drop_stmt = IF(@tables IS NULL, 'SELECT 1', CONCAT('DROP TABLE IF EXISTS ', @tables));
736
736
  PREPARE stmt FROM @drop_stmt;
737
737
  EXECUTE stmt;
@@ -240,13 +240,20 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
240
240
  protected update(def: UpdateQueryDef): QueryBuildResult {
241
241
  const table = this.tableName(def.table);
242
242
  const alias = this.expr.wrap(def.as);
243
+ const hasLimit = def.top != null || def.limit != null;
243
244
 
244
245
  // SET
245
246
  const setParts = Object.entries(def.record).map(
246
247
  ([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
247
248
  );
248
249
 
249
- let sql = `UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
250
+ // CTE: top/limit ctid 기반 서브쿼리
251
+ let ctePart = "";
252
+ if (hasLimit) {
253
+ ctePart = this._buildLimitCte(def, table, alias);
254
+ }
255
+
256
+ let sql = `${ctePart}UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
250
257
 
251
258
  // PostgreSQL: JOIN은 FROM 절로 처리
252
259
  if (def.joins != null && def.joins.length > 0) {
@@ -256,21 +263,29 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
256
263
  });
257
264
  sql += ` FROM ${joinTables.join(", ")}`;
258
265
 
259
- // JOIN ON 조건을 WHERE에 추가
260
266
  const joinConditions = def.joins
261
267
  .filter((j) => j.where != null && j.where.length > 0)
262
268
  .map((j) => this.expr.renderWhere(j.where!));
263
- if (joinConditions.length > 0) {
269
+
270
+ if (hasLimit) {
271
+ // CTE가 WHERE 조건을 포함하므로 ctid 필터 + JOIN 조건만
272
+ const conditions = [`${alias}.ctid IN (SELECT ctid FROM _limited)`, ...joinConditions];
273
+ sql += ` WHERE ${conditions.join(" AND ")}`;
274
+ } else {
264
275
  const whereCondition =
265
276
  def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
266
277
  const allConditions =
267
278
  whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
268
- sql += ` WHERE ${allConditions.join(" AND ")}`;
279
+ if (allConditions.length > 0) {
280
+ sql += ` WHERE ${allConditions.join(" AND ")}`;
281
+ }
282
+ }
283
+ } else {
284
+ if (hasLimit) {
285
+ sql += ` WHERE ${alias}.ctid IN (SELECT ctid FROM _limited)`;
269
286
  } else {
270
287
  sql += this.renderWhere(def.where);
271
288
  }
272
- } else {
273
- sql += this.renderWhere(def.where);
274
289
  }
275
290
 
276
291
  // RETURNING 절
@@ -289,8 +304,15 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
289
304
  protected delete(def: DeleteQueryDef): QueryBuildResult {
290
305
  const table = this.tableName(def.table);
291
306
  const alias = this.expr.wrap(def.as);
307
+ const hasLimit = def.top != null || def.limit != null;
292
308
 
293
- let sql = `DELETE FROM ${table} AS ${alias}`;
309
+ // CTE: top/limit ctid 기반 서브쿼리
310
+ let ctePart = "";
311
+ if (hasLimit) {
312
+ ctePart = this._buildLimitCte(def, table, alias);
313
+ }
314
+
315
+ let sql = `${ctePart}DELETE FROM ${table} AS ${alias}`;
294
316
 
295
317
  // PostgreSQL: JOIN은 USING 절로 처리
296
318
  if (def.joins != null && def.joins.length > 0) {
@@ -300,21 +322,28 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
300
322
  });
301
323
  sql += ` USING ${joinTables.join(", ")}`;
302
324
 
303
- // JOIN ON 조건을 WHERE에 추가
304
325
  const joinConditions = def.joins
305
326
  .filter((j) => j.where != null && j.where.length > 0)
306
327
  .map((j) => this.expr.renderWhere(j.where!));
307
- if (joinConditions.length > 0) {
328
+
329
+ if (hasLimit) {
330
+ const conditions = [`${alias}.ctid IN (SELECT ctid FROM _limited)`, ...joinConditions];
331
+ sql += ` WHERE ${conditions.join(" AND ")}`;
332
+ } else {
308
333
  const whereCondition =
309
334
  def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
310
335
  const allConditions =
311
336
  whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
312
- sql += ` WHERE ${allConditions.join(" AND ")}`;
337
+ if (allConditions.length > 0) {
338
+ sql += ` WHERE ${allConditions.join(" AND ")}`;
339
+ }
340
+ }
341
+ } else {
342
+ if (hasLimit) {
343
+ sql += ` WHERE ${alias}.ctid IN (SELECT ctid FROM _limited)`;
313
344
  } else {
314
345
  sql += this.renderWhere(def.where);
315
346
  }
316
- } else {
317
- sql += this.renderWhere(def.where);
318
347
  }
319
348
 
320
349
  // RETURNING (PostgreSQL: DELETE에서도 지원)
@@ -611,12 +640,62 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
611
640
  protected execProc(def: ExecProcQueryDef): QueryBuildResult {
612
641
  const proc = this.tableName(def.procedure);
613
642
  if (def.params == null || Object.keys(def.params).length === 0) {
614
- return { sql: `SELECT ${proc}()` };
643
+ return { sql: `SELECT * FROM ${proc}()` };
615
644
  }
616
645
  const params = Object.values(def.params)
617
646
  .map((p) => this.expr.render(p))
618
647
  .join(", ");
619
- return { sql: `SELECT ${proc}(${params})` };
648
+ return { sql: `SELECT * FROM ${proc}(${params})` };
649
+ }
650
+
651
+ /**
652
+ * UPDATE/DELETE의 top/limit를 CTE로 변환
653
+ *
654
+ * PostgreSQL은 UPDATE/DELETE에 LIMIT을 지원하지 않으므로
655
+ * ctid 기반 CTE로 변환한다.
656
+ */
657
+ private _buildLimitCte(
658
+ def: { table: QueryDefObjectName; as: string; top?: number; limit?: [number, number]; where?: unknown[]; joins?: SelectQueryDefJoin[] },
659
+ table: string,
660
+ alias: string,
661
+ ): string {
662
+ let sql = `WITH _limited AS (SELECT ${alias}.ctid FROM ${table} AS ${alias}`;
663
+
664
+ if (def.joins != null && def.joins.length > 0) {
665
+ const joinTables = def.joins.map((j) => {
666
+ const from = this.renderFrom(j.from);
667
+ return `${from} AS ${this.expr.wrap(j.as)}`;
668
+ });
669
+ sql += `, ${joinTables.join(", ")}`;
670
+
671
+ const joinConditions = def.joins
672
+ .filter((j) => j.where != null && j.where.length > 0)
673
+ .map((j) => this.expr.renderWhere(j.where!));
674
+ const whereCondition =
675
+ def.where != null && def.where.length > 0
676
+ ? this.expr.renderWhere(def.where as any)
677
+ : null;
678
+ const allConditions = [
679
+ ...(whereCondition != null ? [whereCondition] : []),
680
+ ...joinConditions,
681
+ ];
682
+ if (allConditions.length > 0) {
683
+ sql += ` WHERE ${allConditions.join(" AND ")}`;
684
+ }
685
+ } else if (def.where != null && def.where.length > 0) {
686
+ sql += ` WHERE ${this.expr.renderWhere(def.where as any)}`;
687
+ }
688
+
689
+ if (def.limit != null) {
690
+ const [offset, count] = def.limit;
691
+ sql += ` LIMIT ${count}`;
692
+ if (offset > 0) sql += ` OFFSET ${offset}`;
693
+ } else if (def.top != null) {
694
+ sql += ` LIMIT ${def.top}`;
695
+ }
696
+
697
+ sql += `) `;
698
+ return sql;
620
699
  }
621
700
 
622
701
  //#endregion
@@ -12,6 +12,9 @@ import type { ViewBuilder } from "../view-builder";
12
12
  * 현재 Table에서 대상 Table로의 FK 관계를 정의
13
13
  * DB에 실제 FK 제약조건을 생성
14
14
  *
15
+ * description 설정은 factory 함수의 opts 파라미터로 전달한다.
16
+ * 메서드 체이닝(.description())은 TypeScript 순환 참조 시 TS7022를 유발하므로 제거됨.
17
+ *
15
18
  * @template TOwner - 소유 Table builder 타입
16
19
  * @template TTargetFn - 대상 Table builder factory 타입
17
20
  *
@@ -25,7 +28,7 @@ import type { ViewBuilder } from "../view-builder";
25
28
  * .primaryKey("id")
26
29
  * .relations((r) => ({
27
30
  * // N:1 relationship - Post → User
28
- * author: r.foreignKey(["authorId"], () => User),
31
+ * author: r.foreignKey(["authorId"], () => User, { description: "작성자" }),
29
32
  * }));
30
33
  * ```
31
34
  *
@@ -51,23 +54,16 @@ export class ForeignKeyBuilder<
51
54
  description?: string;
52
55
  },
53
56
  ) {}
54
-
55
- /**
56
- * 관계 설명 설정
57
- *
58
- * @param desc - 관계 설명
59
- * @returns 새 ForeignKeyBuilder 인스턴스
60
- */
61
- description(desc: string): ForeignKeyBuilder<TOwner, TTargetFn> {
62
- return new ForeignKeyBuilder({ ...this.meta, description: desc });
63
- }
64
57
  }
65
58
 
66
59
  /**
67
60
  * Foreign Key 역참조 builder (1:N)
68
61
  *
69
62
  * 다른 Table이 현재 Table을 참조하는 FK의 역참조를 정의
70
- * include() 시 배열로 로드됨 (single() 호출 시 단일 객체)
63
+ * include() 시 배열로 로드됨 (opts.single: true 시 단일 객체)
64
+ *
65
+ * description, single 설정은 factory 함수의 opts 파라미터로 전달한다.
66
+ * 메서드 체이닝(.description(), .single())은 TypeScript 순환 참조 시 TS7022를 유발하므로 제거됨.
71
67
  *
72
68
  * @template TTargetTableFn - 참조하는 Table builder factory 타입
73
69
  * @template TIsSingle - 단일 객체 여부
@@ -85,7 +81,10 @@ export class ForeignKeyBuilder<
85
81
  * posts: r.foreignKeyTarget(() => Post, "author"),
86
82
  *
87
83
  * // 1:1 relation (single object)
88
- * profile: r.foreignKeyTarget(() => Profile, "user").single(),
84
+ * profile: r.foreignKeyTarget(() => Profile, "user", { single: true }),
85
+ *
86
+ * // with description
87
+ * comments: r.foreignKeyTarget(() => Comment, "user", { description: "댓글목록" }),
89
88
  * }));
90
89
  * ```
91
90
  *
@@ -110,32 +109,6 @@ export class ForeignKeyTargetBuilder<
110
109
  isSingle?: TIsSingle;
111
110
  },
112
111
  ) {}
113
-
114
- /**
115
- * 관계 설명 설정
116
- *
117
- * @param desc - 관계 설명
118
- * @returns 새 ForeignKeyTargetBuilder 인스턴스
119
- */
120
- description(desc: string): ForeignKeyTargetBuilder<TTargetTableFn, TIsSingle> {
121
- return new ForeignKeyTargetBuilder({ ...this.meta, description: desc });
122
- }
123
-
124
- /**
125
- * 단일 객체 관계로 설정 (1:1)
126
- *
127
- * 기본값은 배열(1:N), single() 호출 시 단일 객체
128
- *
129
- * @returns 새 ForeignKeyTargetBuilder 인스턴스 (isSingle=true)
130
- *
131
- * @example
132
- * ```typescript
133
- * profile: r.foreignKeyTarget(() => Profile, "user").single()
134
- * ```
135
- */
136
- single(): ForeignKeyTargetBuilder<TTargetTableFn, true> {
137
- return new ForeignKeyTargetBuilder({ ...this.meta, isSingle: true });
138
- }
139
112
  }
140
113
 
141
114
  // ============================================
@@ -148,6 +121,8 @@ export class ForeignKeyTargetBuilder<
148
121
  * ForeignKeyBuilder와 동일하지만 DB에 FK 제약조건을 생성하지 않음
149
122
  * View에서도 사용 가능
150
123
  *
124
+ * description 설정은 factory 함수의 opts 파라미터로 전달한다.
125
+ *
151
126
  * @template TOwner - 소유 Table/View builder 타입
152
127
  * @template TTargetFn - 대상 Table/View builder factory 타입
153
128
  *
@@ -158,14 +133,7 @@ export class ForeignKeyTargetBuilder<
158
133
  * .query((db: MyDb) => db.user().select(...))
159
134
  * .relations((r) => ({
160
135
  * // View → Table (FK 미생성)
161
- * company: r.relationKey(["companyId"], () => Company),
162
- * }));
163
- *
164
- * // FK 없는 Table 관계 정의
165
- * const Report = Table("Report")
166
- * .columns((c) => ({ userId: c.bigint() }))
167
- * .relations((r) => ({
168
- * user: r.relationKey(["userId"], () => User),
136
+ * company: r.relationKey(["companyId"], () => Company, { description: "소속회사" }),
169
137
  * }));
170
138
  * ```
171
139
  *
@@ -190,16 +158,6 @@ export class RelationKeyBuilder<
190
158
  description?: string;
191
159
  },
192
160
  ) {}
193
-
194
- /**
195
- * 관계 설명 설정
196
- *
197
- * @param desc - 관계 설명
198
- * @returns 새 RelationKeyBuilder 인스턴스
199
- */
200
- description(desc: string): RelationKeyBuilder<TOwner, TTargetFn> {
201
- return new RelationKeyBuilder({ ...this.meta, description: desc });
202
- }
203
161
  }
204
162
 
205
163
  /**
@@ -208,6 +166,8 @@ export class RelationKeyBuilder<
208
166
  * ForeignKeyTargetBuilder와 동일하지만 DB에 FK 제약조건을 생성하지 않음
209
167
  * View에서도 사용 가능
210
168
  *
169
+ * description, single 설정은 factory 함수의 opts 파라미터로 전달한다.
170
+ *
211
171
  * @template TTargetTableFn - 참조하는 Table/View builder factory 타입
212
172
  * @template TIsSingle - 단일 객체 여부
213
173
  *
@@ -218,6 +178,8 @@ export class RelationKeyBuilder<
218
178
  * .relations((r) => ({
219
179
  * // 역참조 (FK 미생성)
220
180
  * employees: r.relationKeyTarget(() => UserSummary, "company"),
181
+ * // 단일 객체 + 설명
182
+ * ceo: r.relationKeyTarget(() => UserSummary, "company", { single: true, description: "대표" }),
221
183
  * }));
222
184
  * ```
223
185
  *
@@ -242,27 +204,6 @@ export class RelationKeyTargetBuilder<
242
204
  isSingle?: TIsSingle;
243
205
  },
244
206
  ) {}
245
-
246
- /**
247
- * 관계 설명 설정
248
- *
249
- * @param desc - 관계 설명
250
- * @returns 새 RelationKeyTargetBuilder 인스턴스
251
- */
252
- description(desc: string): RelationKeyTargetBuilder<TTargetTableFn, TIsSingle> {
253
- return new RelationKeyTargetBuilder({ ...this.meta, description: desc });
254
- }
255
-
256
- /**
257
- * 단일 객체 관계로 설정 (1:1)
258
- *
259
- * 기본값은 배열(1:N), single() 호출 시 단일 객체
260
- *
261
- * @returns 새 RelationKeyTargetBuilder 인스턴스 (isSingle=true)
262
- */
263
- single(): RelationKeyTargetBuilder<TTargetTableFn, true> {
264
- return new RelationKeyTargetBuilder({ ...this.meta, isSingle: true });
265
- }
266
207
  }
267
208
 
268
209
  /**
@@ -276,11 +217,18 @@ type RelationFkFactory<TOwner extends TableBuilder<any, any>, TColumnKey extends
276
217
  foreignKey<TTargetFn extends () => TableBuilder<any, any>>(
277
218
  columns: TColumnKey[],
278
219
  targetFn: TTargetFn,
220
+ opts?: { description?: string },
279
221
  ): ForeignKeyBuilder<TOwner, TTargetFn>;
280
- /** 1:N FK 역참조 정의 */
222
+ /** 1:N FK 역참조 정의 (single: true → 단일 객체) */
223
+ foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any>>(
224
+ targetTableFn: TTargetTableFn,
225
+ relationName: string,
226
+ opts: { single: true; description?: string },
227
+ ): ForeignKeyTargetBuilder<TTargetTableFn, true>;
281
228
  foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any>>(
282
229
  targetTableFn: TTargetTableFn,
283
230
  relationName: string,
231
+ opts?: { single?: false; description?: string },
284
232
  ): ForeignKeyTargetBuilder<TTargetTableFn, false>;
285
233
  };
286
234
 
@@ -298,13 +246,22 @@ type RelationRkFactory<
298
246
  relationKey<TTargetFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>>(
299
247
  columns: TColumnKey[],
300
248
  targetFn: TTargetFn,
249
+ opts?: { description?: string },
301
250
  ): RelationKeyBuilder<TOwner, TTargetFn>;
302
- /** 1:N 논리적 역참조 정의 */
251
+ /** 1:N 논리적 역참조 정의 (single: true → 단일 객체) */
303
252
  relationKeyTarget<
304
253
  TTargetTableFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
305
254
  >(
306
255
  targetTableFn: TTargetTableFn,
307
256
  relationName: string,
257
+ opts: { single: true; description?: string },
258
+ ): RelationKeyTargetBuilder<TTargetTableFn, true>;
259
+ relationKeyTarget<
260
+ TTargetTableFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
261
+ >(
262
+ targetTableFn: TTargetTableFn,
263
+ relationName: string,
264
+ opts?: { single?: false; description?: string },
308
265
  ): RelationKeyTargetBuilder<TTargetTableFn, false>;
309
266
  };
310
267
 
@@ -348,25 +305,37 @@ export function createRelationFactory<
348
305
  ? RelationFkFactory<TOwner, TColumnKey> & RelationRkFactory<TOwner, TColumnKey>
349
306
  : RelationRkFactory<TOwner, TColumnKey> {
350
307
  return {
351
- foreignKey(columns, targetFn) {
308
+ foreignKey(columns, targetFn, opts?) {
352
309
  return new ForeignKeyBuilder({
353
310
  ownerFn: ownerFn as () => TableBuilder<any, any>,
354
311
  columns,
355
312
  targetFn,
313
+ description: opts?.description,
356
314
  });
357
315
  },
358
- foreignKeyTarget(targetTableFn, relationName) {
359
- return new ForeignKeyTargetBuilder({ targetTableFn, relationName });
316
+ foreignKeyTarget(targetTableFn, relationName, opts?) {
317
+ return new ForeignKeyTargetBuilder({
318
+ targetTableFn,
319
+ relationName,
320
+ description: opts?.description,
321
+ isSingle: opts?.single,
322
+ });
360
323
  },
361
- relationKey(columns, targetFn) {
324
+ relationKey(columns, targetFn, opts?) {
362
325
  return new RelationKeyBuilder({
363
326
  ownerFn: ownerFn,
364
327
  columns,
365
328
  targetFn,
329
+ description: opts?.description,
366
330
  });
367
331
  },
368
- relationKeyTarget(targetTableFn, relationName) {
369
- return new RelationKeyTargetBuilder({ targetTableFn, relationName });
332
+ relationKeyTarget(targetTableFn, relationName, opts?) {
333
+ return new RelationKeyTargetBuilder({
334
+ targetTableFn,
335
+ relationName,
336
+ description: opts?.description,
337
+ isSingle: opts?.single,
338
+ });
370
339
  },
371
340
  } as TOwner extends TableBuilder<any, any>
372
341
  ? RelationFkFactory<TOwner, TColumnKey> & RelationRkFactory<TOwner, TColumnKey>
@@ -414,7 +383,7 @@ export type ExtractRelationTarget<TRelation> = TRelation extends
414
383
  /**
415
384
  * FKTarget/RelationKeyTarget에서 대상 타입 추출 (배열 또는 단일 객체)
416
385
  *
417
- * 1:N 관계의 대상 타입 (single() 호출 시 단일 객체)
386
+ * 1:N 관계의 대상 타입 (opts.single: true 시 단일 객체)
418
387
  * TTargetTableFn: 순환 참조 방지를 위한 지연 평가용 () => Post 형태
419
388
  *
420
389
  * @template T - FKTarget 또는 RelationKeyTarget builder 타입
@@ -168,9 +168,7 @@ export class ViewBuilder<
168
168
  */
169
169
  relations<T extends RelationBuilderRecord>(
170
170
  fn: (r: ReturnType<typeof createRelationFactory<this, keyof TData & string>>) => T,
171
- ): ViewBuilder<TDbContext, TData & InferDeepRelations<T>, TRelations> {
172
- // TypeScript 제네릭 타입 추론 한계로 인해 캐스팅이 불가피
173
- // TRelations 타입 파라미터와 새로 생성된 관계 타입 T 간의 타입 불일치 해결
171
+ ): ViewBuilder<TDbContext, TData & InferDeepRelations<T>, T> {
174
172
  return new ViewBuilder({
175
173
  ...this.meta,
176
174
  relations: fn(createRelationFactory<this, keyof TData & string>(() => this)),
@@ -157,7 +157,7 @@ export function inferColumnPrimitiveStr(value: ColumnPrimitive): ColumnPrimitive
157
157
  if (value instanceof Time) return "Time";
158
158
  if (value instanceof Uuid) return "Uuid";
159
159
  if (value instanceof Uint8Array) return "Bytes";
160
- throw new Error(`알 없는 타입: ${typeof value}`);
160
+ throw new Error("NULL 값으로는 타입을 추론할 없습니다.");
161
161
  }
162
162
 
163
163
  // ============================================
@@ -4,15 +4,14 @@ import type { ProcedureBuilder } from "../schema/procedure-builder";
4
4
  import type { ColumnBuilder } from "../schema/factory/column-builder";
5
5
  import type { ForeignKeyBuilder } from "../schema/factory/relation-builder";
6
6
  import type { IndexBuilder } from "../schema/factory/index-builder";
7
- import type { DataRecord, IsolationLevel, Migration, ResultMeta } from "./db";
7
+ import type { DataRecord, ResultMeta } from "./db";
8
8
  import type { QueryDef, QueryDefObjectName } from "./query-def";
9
9
 
10
10
  /**
11
11
  * DbContext 핵심 인터페이스
12
12
  *
13
13
  * Queryable, Executable, ViewBuilder에서 사용하는 내부 인터페이스.
14
- * 기존 DbContext 클래스와 새로운 createDbContext 반환 객체 모두
15
- * 이 인터페이스를 만족함.
14
+ * DbContext class가 인터페이스를 구현한다.
16
15
  */
17
16
  export interface DbContextBase {
18
17
  status: DbContextStatus;
@@ -32,63 +31,6 @@ export interface DbContextBase {
32
31
 
33
32
  export type DbContextStatus = "ready" | "connect" | "transact";
34
33
 
35
- /**
36
- * DbContext 정의 (blueprint)
37
- *
38
- * defineDbContext()로 생성됨. Schema 메타데이터만 포함하며 런타임 상태는 없음.
39
- */
40
- export interface DbContextDef<
41
- TTables extends Record<string, TableBuilder<any, any>>,
42
- TViews extends Record<string, ViewBuilder<any, any, any>>,
43
- TProcedures extends Record<string, ProcedureBuilder<any, any>> = {},
44
- > {
45
- readonly meta: {
46
- readonly tables: TTables;
47
- readonly views: TViews;
48
- readonly procedures: TProcedures;
49
- readonly migrations: Migration[];
50
- };
51
- }
52
-
53
- /**
54
- * 전체 DbContext 인스턴스 타입 (createDbContext로 생성)
55
- *
56
- * DbContextBase를 queryable 접근자, DDL 메서드,
57
- * 연결/트랜잭션 관리로 확장.
58
- */
59
- export type DbContextInstance<TDef extends DbContextDef<any, any, any>> = DbContextBase &
60
- DbContextConnectionMethods &
61
- DbContextDdlMethods & {
62
- // 자동 매핑된 table queryable 접근자
63
- [K in keyof TDef["meta"]["tables"]]: () => import("../exec/queryable").Queryable<
64
- TDef["meta"]["tables"][K]["$inferSelect"],
65
- TDef["meta"]["tables"][K]
66
- >;
67
- } & {
68
- // 자동 매핑된 view queryable 접근자
69
- [K in keyof TDef["meta"]["views"]]: () => import("../exec/queryable").Queryable<
70
- TDef["meta"]["views"][K]["$inferSelect"],
71
- never
72
- >;
73
- } & {
74
- // 자동 매핑된 procedure executable 접근자
75
- [K in keyof TDef["meta"]["procedures"]]: () => import("../exec/executable").Executable<
76
- TDef["meta"]["procedures"][K]["$params"],
77
- TDef["meta"]["procedures"][K]["$returns"]
78
- >;
79
- } & {
80
- // 시스템 table
81
- _migration: () => import("../exec/queryable").Queryable<{ code: string }, any>;
82
- // 초기화
83
- initialize(options?: { dbs?: string[]; force?: boolean }): Promise<void>;
84
- };
85
-
86
- export interface DbContextConnectionMethods {
87
- connect<TResult>(fn: () => Promise<TResult>, isolationLevel?: IsolationLevel): Promise<TResult>;
88
- connectWithoutTransaction<TResult>(callback: () => Promise<TResult>): Promise<TResult>;
89
- transaction<TResult>(fn: () => Promise<TResult>, isolationLevel?: IsolationLevel): Promise<TResult>;
90
- }
91
-
92
34
  export interface DbContextDdlMethods {
93
35
  createTable(table: TableBuilder<any, any>): Promise<void>;
94
36
  dropTable(table: QueryDefObjectName): Promise<void>;