@simplysm/orm-common 14.0.47 → 14.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/dist/exec/queryable.d.ts +8 -3
- package/dist/exec/queryable.d.ts.map +1 -1
- package/dist/exec/queryable.js +10 -2
- package/dist/exec/queryable.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/query-builder/base/query-builder-base.d.ts +7 -0
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +9 -0
- package/dist/query-builder/base/query-builder-base.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +3 -0
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +23 -7
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +3 -0
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/utils/pick-result-sets.d.ts +11 -0
- package/dist/utils/pick-result-sets.d.ts.map +1 -0
- package/dist/utils/pick-result-sets.js +23 -0
- package/dist/utils/pick-result-sets.js.map +1 -0
- package/dist/utils/result-parser.d.ts.map +1 -1
- package/dist/utils/result-parser.js +36 -7
- package/dist/utils/result-parser.js.map +1 -1
- package/docs/core.md +188 -0
- package/docs/expression.md +190 -0
- package/docs/models.md +17 -0
- package/docs/query-builder.md +97 -0
- package/docs/queryable-executable.md +311 -0
- package/docs/schema-builders.md +364 -0
- package/docs/types.md +537 -0
- package/package.json +4 -3
- package/src/exec/queryable.ts +15 -4
- package/src/index.ts +1 -0
- package/src/query-builder/base/query-builder-base.ts +16 -6
- package/src/query-builder/mssql/mssql-expr-renderer.ts +3 -3
- package/src/query-builder/mssql/mssql-query-builder.ts +10 -7
- package/src/query-builder/mysql/mysql-expr-renderer.ts +3 -3
- package/src/query-builder/mysql/mysql-query-builder.ts +40 -22
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +3 -3
- package/src/query-builder/postgresql/postgresql-query-builder.ts +10 -7
- package/src/utils/pick-result-sets.ts +30 -0
- package/src/utils/result-parser.ts +56 -22
|
@@ -323,9 +323,9 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
323
323
|
return `SUBSTRING(${this.render(expr.source)}, ${this.render(expr.start)})`;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
protected indexOf(expr: ExprIndexOf): string {
|
|
327
|
-
return `LOCATE(${this.render(expr.search)}, ${this.render(expr.source)})`;
|
|
328
|
-
}
|
|
326
|
+
protected indexOf(expr: ExprIndexOf): string {
|
|
327
|
+
return `(LOCATE(${this.render(expr.search)}, ${this.render(expr.source)}) - 1)`;
|
|
328
|
+
}
|
|
329
329
|
|
|
330
330
|
//#endregion
|
|
331
331
|
|
|
@@ -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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
21
|
-
if (value == null) {
|
|
22
|
-
return
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
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 && !
|
|
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 ||
|
|
470
|
-
return; // 병합할 데이터 없음
|
|
471
|
-
}
|
|
503
|
+
if (newJoinData == null || isEmptyJoinObject(newJoinData)) {
|
|
504
|
+
return; // 병합할 데이터 없음
|
|
505
|
+
}
|
|
472
506
|
|
|
473
507
|
const existingJoinData = existingGroup[localKey];
|
|
474
508
|
|