@simplysm/orm-common 14.0.46 → 14.0.48

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 (54) hide show
  1. package/README.md +269 -0
  2. package/dist/exec/queryable.d.ts +7 -2
  3. package/dist/exec/queryable.d.ts.map +1 -1
  4. package/dist/exec/queryable.js +10 -2
  5. package/dist/exec/queryable.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/query-builder/base/query-builder-base.d.ts +7 -0
  11. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  12. package/dist/query-builder/base/query-builder-base.js +9 -0
  13. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  14. package/dist/query-builder/mssql/mssql-expr-renderer.js +1 -1
  15. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  16. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  17. package/dist/query-builder/mssql/mssql-query-builder.js +3 -0
  18. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  19. package/dist/query-builder/mysql/mysql-expr-renderer.js +1 -1
  20. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  21. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  22. package/dist/query-builder/mysql/mysql-query-builder.js +23 -7
  23. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  24. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +1 -1
  25. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  26. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  27. package/dist/query-builder/postgresql/postgresql-query-builder.js +3 -0
  28. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  29. package/dist/utils/pick-result-sets.d.ts +11 -0
  30. package/dist/utils/pick-result-sets.d.ts.map +1 -0
  31. package/dist/utils/pick-result-sets.js +23 -0
  32. package/dist/utils/pick-result-sets.js.map +1 -0
  33. package/dist/utils/result-parser.d.ts.map +1 -1
  34. package/dist/utils/result-parser.js +36 -7
  35. package/dist/utils/result-parser.js.map +1 -1
  36. package/docs/core.md +188 -0
  37. package/docs/expression.md +190 -0
  38. package/docs/models.md +17 -0
  39. package/docs/query-builder.md +97 -0
  40. package/docs/queryable-executable.md +311 -0
  41. package/docs/schema-builders.md +364 -0
  42. package/docs/types.md +537 -0
  43. package/package.json +4 -3
  44. package/src/exec/queryable.ts +13 -2
  45. package/src/index.ts +1 -0
  46. package/src/query-builder/base/query-builder-base.ts +16 -6
  47. package/src/query-builder/mssql/mssql-expr-renderer.ts +3 -3
  48. package/src/query-builder/mssql/mssql-query-builder.ts +10 -7
  49. package/src/query-builder/mysql/mysql-expr-renderer.ts +3 -3
  50. package/src/query-builder/mysql/mysql-query-builder.ts +40 -22
  51. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +3 -3
  52. package/src/query-builder/postgresql/postgresql-query-builder.ts +10 -7
  53. package/src/utils/pick-result-sets.ts +30 -0
  54. package/src/utils/result-parser.ts +56 -22
@@ -82,13 +82,16 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
82
82
  // 그 외(orderBy, top, select 등)이면 renderFrom(join)으로 서브쿼리 생성
83
83
  const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
84
84
  return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
85
- }
86
-
87
- // 일반 JOIN
88
- const from = this.renderFrom(join.from);
89
- const where =
90
- join.where != null && join.where.length > 0
91
- ? ` ON ${this.expr.renderWhere(join.where)}`
85
+ }
86
+
87
+ // 일반 JOIN
88
+ const from = this.renderFrom(join.from);
89
+ if (this.isRecursiveSelfJoin(join)) {
90
+ return ` CROSS JOIN ${from} AS ${alias}`;
91
+ }
92
+ const where =
93
+ join.where != null && join.where.length > 0
94
+ ? ` ON ${this.expr.renderWhere(join.where)}`
92
95
  : " ON TRUE";
93
96
  return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
94
97
  }
@@ -97,13 +100,13 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
97
100
 
98
101
  //#region ========== DML - SELECT ==========
99
102
 
100
- protected select(def: SelectQueryDef): QueryBuildResult {
103
+ protected select(def: SelectQueryDef): QueryBuildResult {
101
104
  // WITH (CTE)
102
- let sql = "";
103
- if (def.with != null) {
104
- const { name, base, recursive } = def.with;
105
- sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
106
- }
105
+ let sql = "";
106
+ if (def.with != null) {
107
+ const { name, base, recursive } = def.with;
108
+ sql += `WITH RECURSIVE ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
109
+ }
107
110
 
108
111
  // SELECT
109
112
  sql += "SELECT";
@@ -121,11 +124,22 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
121
124
  sql += " *";
122
125
  }
123
126
 
124
- // FROM
125
- if (def.from != null) {
126
- const from = this.renderFrom(def.from);
127
- sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
128
- }
127
+ const recursiveSelfJoin = def.joins?.find((join) => this.isRecursiveSelfJoin(join));
128
+ const remainingJoins =
129
+ recursiveSelfJoin != null ? def.joins?.filter((join) => join !== recursiveSelfJoin) : def.joins;
130
+
131
+ // FROM
132
+ if (def.from != null) {
133
+ if (recursiveSelfJoin != null) {
134
+ const selfFrom = this.renderFrom(recursiveSelfJoin.from);
135
+ const baseFrom = this.renderFrom(def.from);
136
+ sql += ` FROM ${selfFrom} AS ${this.expr.wrap(recursiveSelfJoin.as)}`;
137
+ sql += ` CROSS JOIN ${baseFrom} AS ${this.expr.wrap(def.as)}`;
138
+ } else {
139
+ const from = this.renderFrom(def.from);
140
+ sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
141
+ }
142
+ }
129
143
 
130
144
  // LOCK
131
145
  if (def.lock) {
@@ -133,7 +147,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
133
147
  }
134
148
 
135
149
  // JOINs
136
- sql += this.renderJoins(def.joins);
150
+ sql += this.renderJoins(remainingJoins);
137
151
 
138
152
  // WHERE
139
153
  sql += this.renderWhere(def.where);
@@ -469,6 +483,10 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
469
483
  // INSERT (NOT EXISTS 패턴)
470
484
  const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
471
485
 
486
+ // MySQL은 같은 문장에서 TEMPORARY TABLE을 두 번 참조할 수 없으므로
487
+ // row count를 변수에 저장하여 두 번째 참조를 대체
488
+ const setCntSql = `SET @sd_tmp_cnt = (SELECT COUNT(*) FROM ${tempTableName})`;
489
+
472
490
  // SELECT: UPDATE 결과 또는 INSERT 결과 조회 (UNION ALL로 병합)
473
491
  // UPDATE 경우: 임시 테이블의 PK로 조회
474
492
  const output = def.output;
@@ -487,15 +505,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
487
505
  const pkExpr = def.insertRecord[pk];
488
506
  return `${wrappedPk} = ${this.expr.render(pkExpr)}`;
489
507
  });
490
- const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${tempTableName})`;
508
+ const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND @sd_tmp_cnt = 0`;
491
509
 
492
510
  const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
493
511
 
494
512
  // 임시 테이블 삭제
495
513
  const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
496
514
 
497
- const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
498
- return { sql: statements.join(";\n"), resultSetIndex: 3 };
515
+ const statements = [createTempSql, updateSql, insertSql, setCntSql, selectSql, dropSql];
516
+ return { sql: statements.join(";\n"), resultSetIndex: 4 };
499
517
  }
500
518
 
501
519
  //#endregion
@@ -320,9 +320,9 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
320
320
  return `SUBSTRING(${this.render(expr.source)} FROM ${this.render(expr.start)})`;
321
321
  }
322
322
 
323
- protected indexOf(expr: ExprIndexOf): string {
324
- return `POSITION(${this.render(expr.search)} IN ${this.render(expr.source)})`;
325
- }
323
+ protected indexOf(expr: ExprIndexOf): string {
324
+ return `(POSITION(${this.render(expr.search)} IN ${this.render(expr.source)}) - 1)`;
325
+ }
326
326
 
327
327
  //#endregion
328
328
 
@@ -77,13 +77,16 @@ export class PostgresqlQueryBuilder extends QueryBuilderBase {
77
77
  // 그 외(orderBy, top, select 등)이면 renderFrom(join)으로 서브쿼리 생성
78
78
  const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
79
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)}`
80
+ }
81
+
82
+ // 일반 JOIN
83
+ const from = this.renderFrom(join.from);
84
+ if (this.isRecursiveSelfJoin(join)) {
85
+ return ` CROSS JOIN ${from} AS ${alias}`;
86
+ }
87
+ const where =
88
+ join.where != null && join.where.length > 0
89
+ ? ` ON ${this.expr.renderWhere(join.where)}`
87
90
  : " ON TRUE";
88
91
  return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
89
92
  }
@@ -0,0 +1,30 @@
1
+ import type { QueryBuildResult } from "../types/db";
2
+
3
+ /**
4
+ * 다중 결과 셋에서 QueryBuildResult 메타데이터에 따라 필요한 결과만 추출한다.
5
+ *
6
+ * - `resultSetIndex`가 없으면 첫 번째 셋 반환
7
+ * - `resultSetStride`가 없으면 `resultSetIndex`번째 셋 단일 반환
8
+ * - `resultSetStride`가 있으면 `resultSetIndex`부터 stride 간격으로 모든 셋을 concat하여 반환
9
+ * (MySQL 배치 INSERT: `INSERT;SELECT;INSERT;SELECT;...` 형태에서 SELECT 결과만 모을 때 사용)
10
+ */
11
+ export function pickResultSets<T>(
12
+ rawResults: T[][],
13
+ buildResult: Pick<QueryBuildResult, "resultSetIndex" | "resultSetStride">,
14
+ ): T[] {
15
+ const { resultSetIndex, resultSetStride } = buildResult;
16
+
17
+ if (resultSetIndex == null) {
18
+ return rawResults[0] ?? [];
19
+ }
20
+
21
+ if (resultSetStride == null) {
22
+ return rawResults[resultSetIndex] ?? [];
23
+ }
24
+
25
+ const merged: T[] = [];
26
+ for (let j = resultSetIndex; j < rawResults.length; j += resultSetStride) {
27
+ merged.push(...rawResults[j]);
28
+ }
29
+ return merged;
30
+ }
@@ -16,11 +16,11 @@ declare function setImmediate(callback: () => void): void;
16
16
  * @returns 파싱된 값
17
17
  * @throws 파싱 실패 시 Error
18
18
  */
19
- function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
20
- // null/undefined는 그대로 반환 (key 제거는 호출자가 처리)
21
- if (value == null) {
22
- return undefined;
23
- }
19
+ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
20
+ // undefined는 key 제거 대상으로 남기고, null은 SQL 결과값으로 보존한다.
21
+ if (value == null) {
22
+ return value;
23
+ }
24
24
 
25
25
  switch (type) {
26
26
  case "number": {
@@ -93,13 +93,11 @@ function flatToNested(
93
93
  const result: Record<string, unknown> = {};
94
94
 
95
95
  for (const { key, type, parts } of columnInfos) {
96
- const rawValue = record[key];
97
- const parsedValue = parseValue(rawValue, type);
98
-
99
- // undefined 값은 key로 추가하지 않음
100
- if (parsedValue == null) continue;
101
-
102
- if (parts != null) {
96
+ const rawValue = record[key];
97
+ if (typeof rawValue === "undefined") continue;
98
+ const parsedValue = parseValue(rawValue, type);
99
+
100
+ if (parts != null) {
103
101
  // 중첩 key: "posts.id" → { posts: { id: ... } }
104
102
  let current = result;
105
103
  for (let i = 0; i < parts.length - 1; i++) {
@@ -122,9 +120,45 @@ function flatToNested(
122
120
  /**
123
121
  * 객체가 비어있는지 확인 (모든 값이 undefined)
124
122
  */
125
- function isEmptyObject(record: Record<string, unknown>): boolean {
126
- return Object.keys(record).length === 0;
127
- }
123
+ function isEmptyObject(record: Record<string, unknown>): boolean {
124
+ return Object.keys(record).length === 0;
125
+ }
126
+
127
+ /**
128
+ * JOIN 결과 객체가 실질적으로 비어있는지 확인
129
+ *
130
+ * 모든 값이 null/undefined 이거나, 중첩 객체도 재귀적으로 비어있으면 비어있는 JOIN으로 간주한다.
131
+ */
132
+ function isEmptyJoinObject(record: Record<string, unknown>): boolean {
133
+ const entries = Object.entries(record);
134
+ if (entries.length === 0) {
135
+ return true;
136
+ }
137
+
138
+ for (const [, value] of entries) {
139
+ if (value == null) {
140
+ continue;
141
+ }
142
+
143
+ if (Array.isArray(value)) {
144
+ if (value.length > 0) {
145
+ return false;
146
+ }
147
+ continue;
148
+ }
149
+
150
+ if (typeof value === "object") {
151
+ if (!isEmptyJoinObject(value as Record<string, unknown>)) {
152
+ return false;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ return false;
158
+ }
159
+
160
+ return true;
161
+ }
128
162
 
129
163
  // ============================================
130
164
  // 메인 함수
@@ -367,10 +401,10 @@ function groupRecordsRecursively(
367
401
  const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
368
402
  const joinData = newGroup[localKey] as Record<string, unknown> | undefined;
369
403
 
370
- if (joinData != null && !isEmptyObject(joinData)) {
371
- if (!joinsConfig[joinKey].isSingle) {
372
- // 배열로 변환 (hashSet은 첫 merge 시 초기화)
373
- newGroup[localKey] = [joinData];
404
+ if (joinData != null && !isEmptyJoinObject(joinData)) {
405
+ if (!joinsConfig[joinKey].isSingle) {
406
+ // 배열로 변환 (hashSet은 첫 merge 시 초기화)
407
+ newGroup[localKey] = [joinData];
374
408
  }
375
409
  } else {
376
410
  // 데이터가 비어있으면 key 삭제
@@ -466,9 +500,9 @@ function mergeJoinData(
466
500
  ): void {
467
501
  const newJoinData = newRecord[localKey] as Record<string, unknown> | undefined;
468
502
 
469
- if (newJoinData == null || isEmptyObject(newJoinData)) {
470
- return; // 병합할 데이터 없음
471
- }
503
+ if (newJoinData == null || isEmptyJoinObject(newJoinData)) {
504
+ return; // 병합할 데이터 없음
505
+ }
472
506
 
473
507
  const existingJoinData = existingGroup[localKey];
474
508