@simplysm/orm-common 13.0.100 → 14.0.4
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 +90 -147
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +312 -276
- package/dist/create-db-context.js.map +1 -6
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/column-ddl.js +41 -35
- package/dist/ddl/column-ddl.js.map +1 -6
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +200 -142
- package/dist/ddl/initialize.js.map +1 -6
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/relation-ddl.js +55 -48
- package/dist/ddl/relation-ddl.js.map +1 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/schema-ddl.js +21 -15
- package/dist/ddl/schema-ddl.js.map +1 -6
- package/dist/ddl/table-ddl.d.ts +20 -20
- package/dist/ddl/table-ddl.js +139 -93
- package/dist/ddl/table-ddl.js.map +1 -6
- package/dist/define-db-context.js +10 -13
- package/dist/define-db-context.js.map +1 -6
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/errors/db-transaction-error.js +53 -19
- package/dist/errors/db-transaction-error.js.map +1 -6
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +94 -40
- package/dist/exec/executable.js.map +1 -6
- package/dist/exec/queryable.d.ts +97 -97
- package/dist/exec/queryable.js +1310 -1204
- package/dist/exec/queryable.js.map +1 -6
- package/dist/exec/search-parser.d.ts +31 -31
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/exec/search-parser.js +158 -59
- package/dist/exec/search-parser.js.map +1 -6
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr-unit.js +24 -18
- package/dist/expr/expr-unit.js.map +1 -6
- package/dist/expr/expr.d.ts +108 -108
- package/dist/expr/expr.js +1872 -1844
- package/dist/expr/expr.js.map +1 -6
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -6
- package/dist/models/system-migration.js +7 -7
- package/dist/models/system-migration.js.map +1 -6
- package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
- package/dist/query-builder/base/expr-renderer-base.js +27 -21
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
- package/dist/query-builder/base/query-builder-base.d.ts +21 -21
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +90 -80
- package/dist/query-builder/base/query-builder-base.js.map +1 -6
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/query-builder/query-builder.js +13 -13
- package/dist/query-builder/query-builder.js.map +1 -6
- package/dist/schema/factory/column-builder.d.ts +84 -84
- package/dist/schema/factory/column-builder.js +248 -185
- package/dist/schema/factory/column-builder.js.map +1 -6
- package/dist/schema/factory/index-builder.d.ts +38 -38
- package/dist/schema/factory/index-builder.js +144 -85
- package/dist/schema/factory/index-builder.js.map +1 -6
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +274 -136
- package/dist/schema/factory/relation-builder.js.map +1 -6
- package/dist/schema/procedure-builder.d.ts +51 -51
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +205 -131
- package/dist/schema/procedure-builder.js.map +1 -6
- package/dist/schema/table-builder.d.ts +55 -55
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +274 -205
- package/dist/schema/table-builder.js.map +1 -6
- package/dist/schema/view-builder.d.ts +44 -44
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +189 -116
- package/dist/schema/view-builder.js.map +1 -6
- package/dist/types/column.d.ts +21 -21
- package/dist/types/column.js +60 -30
- package/dist/types/column.js.map +1 -6
- package/dist/types/db-context-def.d.ts +9 -9
- package/dist/types/db-context-def.js +2 -1
- package/dist/types/db-context-def.js.map +1 -6
- package/dist/types/db.d.ts +47 -47
- package/dist/types/db.js +15 -5
- package/dist/types/db.js.map +1 -6
- package/dist/types/expr.d.ts +81 -81
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/expr.js +3 -1
- package/dist/types/expr.js.map +1 -6
- package/dist/types/query-def.d.ts +46 -46
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/types/query-def.js +31 -24
- package/dist/types/query-def.js.map +1 -6
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/docs/core.md +117 -145
- package/docs/expression.md +186 -203
- package/docs/query-builder.md +75 -42
- package/docs/queryable.md +189 -151
- package/docs/schema-builders.md +172 -283
- package/docs/types.md +229 -173
- package/package.json +7 -5
- package/src/create-db-context.ts +31 -31
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +38 -38
- package/src/ddl/relation-ddl.ts +6 -6
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +24 -24
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +152 -152
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +118 -118
- package/src/index.ts +8 -8
- package/src/models/system-migration.ts +1 -1
- package/src/query-builder/base/expr-renderer-base.ts +21 -21
- package/src/query-builder/base/query-builder-base.ts +33 -33
- package/src/query-builder/mssql/mssql-expr-renderer.ts +28 -28
- package/src/query-builder/mssql/mssql-query-builder.ts +37 -37
- package/src/query-builder/mysql/mysql-expr-renderer.ts +29 -29
- package/src/query-builder/mysql/mysql-query-builder.ts +70 -70
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +22 -22
- package/src/query-builder/postgresql/postgresql-query-builder.ts +54 -54
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +86 -86
- package/src/schema/factory/index-builder.ts +38 -38
- package/src/schema/factory/relation-builder.ts +102 -102
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +47 -47
- package/src/types/column.ts +24 -24
- package/src/types/db-context-def.ts +15 -15
- package/src/types/db.ts +50 -50
- package/src/types/expr.ts +103 -103
- package/src/types/query-def.ts +50 -50
- package/src/utils/result-parser.ts +88 -88
- package/docs/utilities.md +0 -27
- package/tests/db-context/create-db-context.spec.ts +0 -193
- package/tests/db-context/define-db-context.spec.ts +0 -17
- package/tests/ddl/basic.expected.ts +0 -341
- package/tests/ddl/basic.spec.ts +0 -557
- package/tests/ddl/column-builder.expected.ts +0 -310
- package/tests/ddl/column-builder.spec.ts +0 -525
- package/tests/ddl/index-builder.expected.ts +0 -38
- package/tests/ddl/index-builder.spec.ts +0 -148
- package/tests/ddl/procedure-builder.expected.ts +0 -52
- package/tests/ddl/procedure-builder.spec.ts +0 -128
- package/tests/ddl/relation-builder.expected.ts +0 -36
- package/tests/ddl/relation-builder.spec.ts +0 -171
- package/tests/ddl/table-builder.expected.ts +0 -113
- package/tests/ddl/table-builder.spec.ts +0 -399
- package/tests/ddl/view-builder.expected.ts +0 -38
- package/tests/ddl/view-builder.spec.ts +0 -116
- package/tests/dml/delete.expected.ts +0 -96
- package/tests/dml/delete.spec.ts +0 -127
- package/tests/dml/insert.expected.ts +0 -192
- package/tests/dml/insert.spec.ts +0 -210
- package/tests/dml/update.expected.ts +0 -176
- package/tests/dml/update.spec.ts +0 -222
- package/tests/dml/upsert.expected.ts +0 -215
- package/tests/dml/upsert.spec.ts +0 -190
- package/tests/errors/queryable-errors.spec.ts +0 -126
- package/tests/escape.spec.ts +0 -59
- package/tests/examples/pivot.expected.ts +0 -211
- package/tests/examples/pivot.spec.ts +0 -200
- package/tests/examples/sampling.expected.ts +0 -69
- package/tests/examples/sampling.spec.ts +0 -42
- package/tests/examples/unpivot.expected.ts +0 -120
- package/tests/examples/unpivot.spec.ts +0 -161
- package/tests/exec/search-parser.spec.ts +0 -267
- package/tests/executable/basic.expected.ts +0 -18
- package/tests/executable/basic.spec.ts +0 -54
- package/tests/expr/comparison.expected.ts +0 -282
- package/tests/expr/comparison.spec.ts +0 -334
- package/tests/expr/conditional.expected.ts +0 -134
- package/tests/expr/conditional.spec.ts +0 -249
- package/tests/expr/date.expected.ts +0 -332
- package/tests/expr/date.spec.ts +0 -459
- package/tests/expr/math.expected.ts +0 -62
- package/tests/expr/math.spec.ts +0 -59
- package/tests/expr/string.expected.ts +0 -218
- package/tests/expr/string.spec.ts +0 -300
- package/tests/expr/utility.expected.ts +0 -147
- package/tests/expr/utility.spec.ts +0 -155
- package/tests/select/basic.expected.ts +0 -322
- package/tests/select/basic.spec.ts +0 -433
- package/tests/select/filter.expected.ts +0 -357
- package/tests/select/filter.spec.ts +0 -954
- package/tests/select/group.expected.ts +0 -169
- package/tests/select/group.spec.ts +0 -159
- package/tests/select/join.expected.ts +0 -582
- package/tests/select/join.spec.ts +0 -692
- package/tests/select/order.expected.ts +0 -150
- package/tests/select/order.spec.ts +0 -140
- package/tests/select/recursive-cte.expected.ts +0 -244
- package/tests/select/recursive-cte.spec.ts +0 -514
- package/tests/select/result-meta.spec.ts +0 -270
- package/tests/select/subquery.expected.ts +0 -363
- package/tests/select/subquery.spec.ts +0 -441
- package/tests/select/view.expected.ts +0 -155
- package/tests/select/view.spec.ts +0 -235
- package/tests/select/window.expected.ts +0 -345
- package/tests/select/window.spec.ts +0 -433
- package/tests/setup/MockExecutor.ts +0 -18
- package/tests/setup/TestDbContext.ts +0 -59
- package/tests/setup/models/Company.ts +0 -13
- package/tests/setup/models/Employee.ts +0 -10
- package/tests/setup/models/MonthlySales.ts +0 -11
- package/tests/setup/models/Post.ts +0 -16
- package/tests/setup/models/Sales.ts +0 -10
- package/tests/setup/models/User.ts +0 -19
- package/tests/setup/procedure/GetAllUsers.ts +0 -9
- package/tests/setup/procedure/GetUserById.ts +0 -12
- package/tests/setup/test-utils.ts +0 -72
- package/tests/setup/views/ActiveUsers.ts +0 -8
- package/tests/setup/views/UserSummary.ts +0 -11
- package/tests/types/nullable-queryable-record.spec.ts +0 -97
- package/tests/utils/result-parser-perf.spec.ts +0 -143
- package/tests/utils/result-parser.spec.ts +0 -667
|
@@ -1,265 +1,406 @@
|
|
|
1
1
|
import { bytes, obj, DateOnly, DateTime, Time, Uuid } from "@simplysm/core-common";
|
|
2
|
+
// ============================================
|
|
3
|
+
// 타입 파서
|
|
4
|
+
// ============================================
|
|
5
|
+
/**
|
|
6
|
+
* 값을 지정된 타입으로 파싱
|
|
7
|
+
*
|
|
8
|
+
* @param value - 파싱할 값
|
|
9
|
+
* @param type - 대상 타입 (ColumnPrimitiveStr)
|
|
10
|
+
* @returns 파싱된 값
|
|
11
|
+
* @throws 파싱 실패 시 Error
|
|
12
|
+
*/
|
|
2
13
|
function parseValue(value, type) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
// null/undefined는 그대로 반환 (key 제거는 호출자가 처리)
|
|
15
|
+
if (value == null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
switch (type) {
|
|
19
|
+
case "number": {
|
|
20
|
+
const num = Number(value);
|
|
21
|
+
if (Number.isNaN(num)) {
|
|
22
|
+
throw new Error(`숫자 파싱 실패: ${String(value)}`);
|
|
23
|
+
}
|
|
24
|
+
return num;
|
|
25
|
+
}
|
|
26
|
+
case "string":
|
|
27
|
+
return String(value);
|
|
28
|
+
case "boolean":
|
|
29
|
+
// 0, 1, "0", "1", true, false 등 처리
|
|
30
|
+
if (value === 0 || value === "0" || value === false)
|
|
31
|
+
return false;
|
|
32
|
+
if (value === 1 || value === "1" || value === true)
|
|
33
|
+
return true;
|
|
34
|
+
return Boolean(value);
|
|
35
|
+
case "DateTime":
|
|
36
|
+
return DateTime.parse(value);
|
|
37
|
+
case "DateOnly":
|
|
38
|
+
return DateOnly.parse(value);
|
|
39
|
+
case "Time":
|
|
40
|
+
return Time.parse(value);
|
|
41
|
+
case "Uuid":
|
|
42
|
+
if (value instanceof Uint8Array)
|
|
43
|
+
return Uuid.fromBytes(value);
|
|
44
|
+
return new Uuid(value);
|
|
45
|
+
case "Bytes":
|
|
46
|
+
if (value instanceof Uint8Array)
|
|
47
|
+
return value;
|
|
48
|
+
if (typeof value === "string")
|
|
49
|
+
return bytes.fromHex(value);
|
|
50
|
+
throw new Error(`Bytes 파싱 실패: ${typeof value}`);
|
|
13
51
|
}
|
|
14
|
-
case "string":
|
|
15
|
-
return String(value);
|
|
16
|
-
case "boolean":
|
|
17
|
-
if (value === 0 || value === "0" || value === false) return false;
|
|
18
|
-
if (value === 1 || value === "1" || value === true) return true;
|
|
19
|
-
return Boolean(value);
|
|
20
|
-
case "DateTime":
|
|
21
|
-
return DateTime.parse(value);
|
|
22
|
-
case "DateOnly":
|
|
23
|
-
return DateOnly.parse(value);
|
|
24
|
-
case "Time":
|
|
25
|
-
return Time.parse(value);
|
|
26
|
-
case "Uuid":
|
|
27
|
-
if (value instanceof Uint8Array) return Uuid.fromBytes(value);
|
|
28
|
-
return new Uuid(value);
|
|
29
|
-
case "Bytes":
|
|
30
|
-
if (value instanceof Uint8Array) return value;
|
|
31
|
-
if (typeof value === "string") return bytes.fromHex(value);
|
|
32
|
-
throw new Error(`Failed to parse Bytes: ${typeof value}`);
|
|
33
|
-
}
|
|
34
52
|
}
|
|
53
|
+
/** 고유한 columns 객체마다 column 정보를 한 번만 사전 계산 */
|
|
35
54
|
function buildColumnInfos(columns) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
return Object.entries(columns).map(([key, type]) => ({
|
|
56
|
+
key,
|
|
57
|
+
type,
|
|
58
|
+
parts: key.includes(".") ? key.split(".") : undefined,
|
|
59
|
+
}));
|
|
41
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* 플랫 레코드를 중첩 객체로 변환
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* { "posts.id": 1, "posts.title": "Hi" } → { posts: { id: 1, title: "Hi" } }
|
|
66
|
+
*/
|
|
42
67
|
function flatToNested(record, columnInfos) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
const result = {};
|
|
69
|
+
for (const { key, type, parts } of columnInfos) {
|
|
70
|
+
const rawValue = record[key];
|
|
71
|
+
const parsedValue = parseValue(rawValue, type);
|
|
72
|
+
// undefined 값은 key로 추가하지 않음
|
|
73
|
+
if (parsedValue === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
if (parts != null) {
|
|
76
|
+
// 중첩 key: "posts.id" → { posts: { id: ... } }
|
|
77
|
+
let current = result;
|
|
78
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
79
|
+
const part = parts[i];
|
|
80
|
+
if (current[part] == null) {
|
|
81
|
+
current[part] = {};
|
|
82
|
+
}
|
|
83
|
+
current = current[part];
|
|
84
|
+
}
|
|
85
|
+
current[parts[parts.length - 1]] = parsedValue;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// 단순 key
|
|
89
|
+
result[key] = parsedValue;
|
|
54
90
|
}
|
|
55
|
-
current = current[part];
|
|
56
|
-
}
|
|
57
|
-
current[parts[parts.length - 1]] = parsedValue;
|
|
58
|
-
} else {
|
|
59
|
-
result[key] = parsedValue;
|
|
60
91
|
}
|
|
61
|
-
|
|
62
|
-
return result;
|
|
92
|
+
return result;
|
|
63
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* 객체가 비어있는지 확인 (모든 값이 undefined)
|
|
96
|
+
*/
|
|
64
97
|
function isEmptyObject(record) {
|
|
65
|
-
|
|
98
|
+
return Object.keys(record).length === 0;
|
|
66
99
|
}
|
|
100
|
+
// ============================================
|
|
101
|
+
// 메인 함수
|
|
102
|
+
// ============================================
|
|
103
|
+
/** 양보 간격: N개 레코드마다 이벤트 루프에 양보 */
|
|
67
104
|
const YIELD_INTERVAL = 100;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
/** 이벤트 루프 양보: Node.js는 setImmediate, 브라우저는 setTimeout 폴백 */
|
|
106
|
+
const yieldToEventLoop = typeof setImmediate !== "undefined"
|
|
107
|
+
? () => new Promise((resolve) => setImmediate(resolve))
|
|
108
|
+
: () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
109
|
+
/**
|
|
110
|
+
* ResultMeta를 통해 DB 쿼리 결과를 TypeScript 객체로 변환
|
|
111
|
+
*
|
|
112
|
+
* @param rawResults - 데이터베이스에서 반환된 원시 결과 배열
|
|
113
|
+
* @param meta - 타입 변환 및 JOIN 구조 정보 (필수)
|
|
114
|
+
* @returns 타입 변환 및 중첩된 결과 배열. 입력이 비어있거나 유효한 결과가 없으면 undefined 반환
|
|
115
|
+
* @throws 타입 파싱 실패 시 Error
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* - meta 필수: meta 없이는 이 함수를 호출할 필요 없음 (입력 = 출력)
|
|
119
|
+
* - async 전용: 대규모 처리 시 외부 인터럽트 허용을 위해 동기 버전 미제공
|
|
120
|
+
* - 브라우저/Node 호환: setTimeout(resolve, 0)으로 양보
|
|
121
|
+
* - 빈 결과 처리: 입력 배열이 비어있거나 파싱 후 모든 레코드가 빈 객체이면 undefined 반환
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* // 1. 단순 타입 파싱
|
|
126
|
+
* const raw = [{ id: "1", createdAt: "2026-01-07T10:00:00.000Z" }];
|
|
127
|
+
* const meta = { columns: { id: "number", createdAt: "DateTime" }, joins: {} };
|
|
128
|
+
* const result = await parseQueryResult(raw, meta);
|
|
129
|
+
* // [{ id: 1, createdAt: DateTime(...) }]
|
|
130
|
+
*
|
|
131
|
+
* // 2. JOIN 결과 중첩
|
|
132
|
+
* const raw = [
|
|
133
|
+
* { id: 1, name: "User1", "posts.id": 10, "posts.title": "Post1" },
|
|
134
|
+
* { id: 1, name: "User1", "posts.id": 11, "posts.title": "Post2" },
|
|
135
|
+
* ];
|
|
136
|
+
* const meta = {
|
|
137
|
+
* columns: { id: "number", name: "string", "posts.id": "number", "posts.title": "string" },
|
|
138
|
+
* joins: { posts: { isSingle: false } }
|
|
139
|
+
* };
|
|
140
|
+
* const result = await parseQueryResult(raw, meta);
|
|
141
|
+
* // [{ id: 1, name: "User1", posts: [{ id: 10, title: "Post1" }, { id: 11, title: "Post2" }] }]
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export async function parseQueryResult(rawResults, meta) {
|
|
145
|
+
// 빈 입력 처리
|
|
146
|
+
if (rawResults.length === 0) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const joinKeys = Object.keys(meta.joins);
|
|
150
|
+
// JOIN 없음: 단순 타입 파싱만 수행
|
|
151
|
+
if (joinKeys.length === 0) {
|
|
152
|
+
return parseSimpleRecords(rawResults, meta.columns);
|
|
153
|
+
}
|
|
154
|
+
// JOIN 있음: 그룹핑 + 중첩
|
|
155
|
+
return parseJoinedRecords(rawResults, meta);
|
|
78
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* JOIN이 없는 단순 레코드 파싱
|
|
159
|
+
*/
|
|
79
160
|
async function parseSimpleRecords(rawResults, columns) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
161
|
+
const columnInfos = buildColumnInfos(columns);
|
|
162
|
+
const results = [];
|
|
163
|
+
for (let i = 0; i < rawResults.length; i++) {
|
|
164
|
+
// 이벤트 루프에 양보
|
|
165
|
+
if (i > 0 && i % YIELD_INTERVAL === 0) {
|
|
166
|
+
await yieldToEventLoop();
|
|
167
|
+
}
|
|
168
|
+
const parsed = flatToNested(rawResults[i], columnInfos);
|
|
169
|
+
// 빈 객체 제외
|
|
170
|
+
if (!isEmptyObject(parsed)) {
|
|
171
|
+
results.push(parsed);
|
|
172
|
+
}
|
|
89
173
|
}
|
|
90
|
-
|
|
91
|
-
|
|
174
|
+
// 빈 배열은 undefined 반환
|
|
175
|
+
return results.length > 0 ? results : undefined;
|
|
92
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* JOIN key를 깊이순으로 정렬 (얕은 것 우선)
|
|
179
|
+
* "posts" (1) < "posts.comments" (2)
|
|
180
|
+
*/
|
|
93
181
|
function sortJoinKeysByDepth(joinKeys) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
182
|
+
return [...joinKeys].sort((a, b) => {
|
|
183
|
+
const depthA = a.split(".").length;
|
|
184
|
+
const depthB = b.split(".").length;
|
|
185
|
+
return depthA - depthB; // 얕은 것 우선
|
|
186
|
+
});
|
|
99
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* JOIN이 있는 레코드 파싱 (재귀 그룹핑)
|
|
190
|
+
*/
|
|
100
191
|
async function parseJoinedRecords(rawResults, meta) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
// 1. 모든 레코드를 중첩 구조로 변환
|
|
193
|
+
const columnInfos = buildColumnInfos(meta.columns);
|
|
194
|
+
const nestedRecords = [];
|
|
195
|
+
for (let i = 0; i < rawResults.length; i++) {
|
|
196
|
+
if (i > 0 && i % YIELD_INTERVAL === 0) {
|
|
197
|
+
await yieldToEventLoop();
|
|
198
|
+
}
|
|
199
|
+
nestedRecords.push(flatToNested(rawResults[i], columnInfos));
|
|
106
200
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
201
|
+
// 2. JOIN key를 깊이순으로 정렬 (얕은 것 우선)
|
|
202
|
+
const sortedJoinKeys = sortJoinKeysByDepth(Object.keys(meta.joins));
|
|
203
|
+
// 3. 루트 레벨부터 재귀적으로 그룹핑
|
|
204
|
+
const results = groupRecordsRecursively(nestedRecords, sortedJoinKeys, meta.joins, "");
|
|
205
|
+
// 4. 빈 결과 필터링
|
|
206
|
+
const filteredResults = results.filter((r) => !isEmptyObject(r));
|
|
207
|
+
return filteredResults.length > 0 ? filteredResults : undefined;
|
|
113
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* 그룹 key를 문자열로 직렬화 (Map key로 사용)
|
|
211
|
+
*
|
|
212
|
+
* JSON.stringify보다 빠른 커스텀 직렬화
|
|
213
|
+
*/
|
|
114
214
|
function serializeGroupKey(groupKey, cachedKeyOrder) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
215
|
+
const keys = cachedKeyOrder ?? Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
|
|
216
|
+
let result = "";
|
|
217
|
+
for (let i = 0; i < keys.length; i++) {
|
|
218
|
+
if (i > 0)
|
|
219
|
+
result += "|";
|
|
220
|
+
const v = groupKey[keys[i]];
|
|
221
|
+
result += keys[i];
|
|
222
|
+
result += ":";
|
|
223
|
+
result += v === null ? "null" : String(v);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
125
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* 현재 경로에 대해 레코드를 재귀적으로 그룹핑
|
|
229
|
+
*
|
|
230
|
+
* Map 기반 그룹핑으로 O(n) 복잡도 달성
|
|
231
|
+
*
|
|
232
|
+
* @param records - 그룹핑할 레코드 배열
|
|
233
|
+
* @param allJoinKeys - 모든 JOIN key (깊이순 정렬)
|
|
234
|
+
* @param joinsConfig - JOIN 설정
|
|
235
|
+
* @param currentPath - 현재 경로 (예: "", "posts", "posts.comments")
|
|
236
|
+
*/
|
|
126
237
|
function groupRecordsRecursively(records, allJoinKeys, joinsConfig, currentPath) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
groupKeyOrder = Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
|
|
238
|
+
// 현재 경로에 직접 대응하는 JOIN key 찾기
|
|
239
|
+
// 예: currentPath="" → ["posts", "company"]
|
|
240
|
+
// 예: currentPath="posts" → ["posts.comments"]
|
|
241
|
+
const childJoinKeys = allJoinKeys.filter((key) => {
|
|
242
|
+
if (currentPath === "") {
|
|
243
|
+
// 루트 레벨: 점이 없는 key
|
|
244
|
+
return !key.includes(".");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// 하위 레벨: 현재 경로 + "." + key
|
|
248
|
+
return (key.startsWith(currentPath + ".") && key.slice(currentPath.length + 1).indexOf(".") === -1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
if (childJoinKeys.length === 0) {
|
|
252
|
+
// 더 이상 그룹핑할 JOIN 없음
|
|
253
|
+
return records;
|
|
144
254
|
}
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
255
|
+
// Map 기반 그룹핑 (O(n) 복잡도)
|
|
256
|
+
const groupMap = new Map();
|
|
257
|
+
// O(1) 조회를 위한 JOIN key 제외 집합 사전 계산
|
|
258
|
+
const joinKeyExclusions = buildJoinKeyExclusionSet(childJoinKeys);
|
|
259
|
+
// Key 순서 캐싱 (첫 번째 레코드에서 결정 후 재사용)
|
|
260
|
+
let groupKeyOrder;
|
|
261
|
+
for (const record of records) {
|
|
262
|
+
// 그룹 key 추출 및 직렬화 (JOIN key 제외)
|
|
263
|
+
const groupKey = extractGroupKey(record, joinKeyExclusions);
|
|
264
|
+
if (groupKeyOrder == null) {
|
|
265
|
+
groupKeyOrder = Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
|
|
266
|
+
}
|
|
267
|
+
const keyStr = serializeGroupKey(groupKey, groupKeyOrder);
|
|
268
|
+
const existingGroup = groupMap.get(keyStr);
|
|
269
|
+
if (existingGroup != null) {
|
|
270
|
+
// 기존 그룹에 JOIN 데이터 병합
|
|
271
|
+
for (const joinKey of childJoinKeys) {
|
|
272
|
+
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
273
|
+
mergeJoinData(existingGroup, record, localKey, joinsConfig[joinKey].isSingle);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// 새 그룹 생성
|
|
278
|
+
const newGroup = { ...record };
|
|
279
|
+
// 각 JOIN key를 배열 또는 단일 객체로 초기화
|
|
280
|
+
for (const joinKey of childJoinKeys) {
|
|
281
|
+
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
282
|
+
const joinData = newGroup[localKey];
|
|
283
|
+
if (joinData != null && !isEmptyObject(joinData)) {
|
|
284
|
+
if (!joinsConfig[joinKey].isSingle) {
|
|
285
|
+
// 배열로 변환
|
|
286
|
+
newGroup[localKey] = [joinData];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// 데이터가 비어있으면 key 삭제
|
|
291
|
+
delete newGroup[localKey];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
groupMap.set(keyStr, newGroup);
|
|
163
295
|
}
|
|
164
|
-
}
|
|
165
|
-
groupMap.set(keyStr, newGroup);
|
|
166
296
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
for (const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
joinKey
|
|
186
|
-
);
|
|
187
|
-
if (processed.length > 0) {
|
|
188
|
-
group[localKey] = processed[0];
|
|
297
|
+
// Map을 배열로 변환
|
|
298
|
+
const grouped = Array.from(groupMap.values());
|
|
299
|
+
// 각 JOIN의 하위 레벨을 재귀적으로 처리
|
|
300
|
+
for (const group of grouped) {
|
|
301
|
+
for (const joinKey of childJoinKeys) {
|
|
302
|
+
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
303
|
+
const joinData = group[localKey];
|
|
304
|
+
if (Array.isArray(joinData) && joinData.length > 0) {
|
|
305
|
+
// 배열인 경우: 하위 레벨을 재귀적으로 처리
|
|
306
|
+
group[localKey] = groupRecordsRecursively(joinData, allJoinKeys, joinsConfig, joinKey);
|
|
307
|
+
}
|
|
308
|
+
else if (joinData != null && typeof joinData === "object" && !Array.isArray(joinData)) {
|
|
309
|
+
// 단일 객체인 경우 (isSingle: true)
|
|
310
|
+
const processed = groupRecordsRecursively([joinData], allJoinKeys, joinsConfig, joinKey);
|
|
311
|
+
if (processed.length > 0) {
|
|
312
|
+
group[localKey] = processed[0];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
189
315
|
}
|
|
190
|
-
}
|
|
191
316
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
317
|
+
// __hashSet__ 내부 속성 제거 (중복 검사용 임시 속성)
|
|
318
|
+
for (const group of grouped) {
|
|
319
|
+
for (const key of Object.keys(group)) {
|
|
320
|
+
if (key.startsWith("__hashSet__")) {
|
|
321
|
+
delete group[key];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
198
324
|
}
|
|
199
|
-
|
|
200
|
-
return grouped;
|
|
325
|
+
return grouped;
|
|
201
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* 그룹 key에서 제외할 key의 Set 구성 (join key와 그 접두사)
|
|
329
|
+
*/
|
|
202
330
|
function buildJoinKeyExclusionSet(joinKeys) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
331
|
+
const exclusions = new Set();
|
|
332
|
+
for (const jk of joinKeys) {
|
|
333
|
+
exclusions.add(jk);
|
|
334
|
+
// 상위 경로도 제외 (예: join key "posts.comments"에 대해 "posts")
|
|
335
|
+
const parts = jk.split(".");
|
|
336
|
+
for (let i = 1; i < parts.length; i++) {
|
|
337
|
+
exclusions.add(parts.slice(0, i).join("."));
|
|
338
|
+
}
|
|
209
339
|
}
|
|
210
|
-
|
|
211
|
-
return exclusions;
|
|
340
|
+
return exclusions;
|
|
212
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* JOIN key를 제외하고 레코드에서 그룹 key 추출
|
|
344
|
+
*/
|
|
213
345
|
function extractGroupKey(record, joinKeyExclusions) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
346
|
+
const result = {};
|
|
347
|
+
for (const [key, value] of Object.entries(record)) {
|
|
348
|
+
// JOIN이 아닌 key만 포함
|
|
349
|
+
if (!joinKeyExclusions.has(key)) {
|
|
350
|
+
// 프리미티브 값만 그룹 key로 사용 (객체/배열 제외)
|
|
351
|
+
if (value == null || typeof value !== "object") {
|
|
352
|
+
result[key] = value;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
220
355
|
}
|
|
221
|
-
|
|
222
|
-
return result;
|
|
356
|
+
return result;
|
|
223
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* 기존 그룹에 JOIN 데이터 병합
|
|
360
|
+
*/
|
|
224
361
|
function mergeJoinData(existingGroup, newRecord, localKey, isSingle) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
362
|
+
const newJoinData = newRecord[localKey];
|
|
363
|
+
if (newJoinData == null || isEmptyObject(newJoinData)) {
|
|
364
|
+
return; // 병합할 데이터 없음
|
|
365
|
+
}
|
|
366
|
+
const existingJoinData = existingGroup[localKey];
|
|
367
|
+
if (isSingle) {
|
|
368
|
+
// isSingle: true - 데이터가 존재하고 값이 다르면 에러
|
|
369
|
+
if (existingJoinData != null) {
|
|
370
|
+
if (!obj.equal(existingJoinData, newJoinData)) {
|
|
371
|
+
throw new Error(`isSingle 관계 '${localKey}'에 여러 개의 다른 결과가 있습니다.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
existingGroup[localKey] = newJoinData;
|
|
376
|
+
}
|
|
237
377
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const newHash = serializeGroupKey(newJoinData);
|
|
246
|
-
if (hashSet != null) {
|
|
247
|
-
if (!hashSet.has(newHash)) {
|
|
248
|
-
hashSet.add(newHash);
|
|
249
|
-
existingJoinData.push(newJoinData);
|
|
378
|
+
else {
|
|
379
|
+
// isSingle: false → 배열에 추가
|
|
380
|
+
const hashSetKey = `__hashSet__${localKey}`;
|
|
381
|
+
if (!Array.isArray(existingJoinData)) {
|
|
382
|
+
existingGroup[localKey] = [newJoinData];
|
|
383
|
+
// Set 기반 중복 검사용 내부 속성 초기화
|
|
384
|
+
existingGroup[hashSetKey] = new Set([serializeGroupKey(newJoinData)]);
|
|
250
385
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
386
|
+
else {
|
|
387
|
+
// Set 기반 중복 검사 (O(1))
|
|
388
|
+
const hashSet = existingGroup[hashSetKey];
|
|
389
|
+
const newHash = serializeGroupKey(newJoinData);
|
|
390
|
+
if (hashSet != null) {
|
|
391
|
+
if (!hashSet.has(newHash)) {
|
|
392
|
+
hashSet.add(newHash);
|
|
393
|
+
existingJoinData.push(newJoinData);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// hashSet 없는 폴백 (레거시 방식)
|
|
398
|
+
const isDuplicate = existingJoinData.some((item) => obj.equal(item, newJoinData));
|
|
399
|
+
if (!isDuplicate) {
|
|
400
|
+
existingJoinData.push(newJoinData);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
257
403
|
}
|
|
258
|
-
}
|
|
259
404
|
}
|
|
260
|
-
}
|
|
261
405
|
}
|
|
262
|
-
|
|
263
|
-
parseQueryResult
|
|
264
|
-
};
|
|
265
|
-
//# sourceMappingURL=result-parser.js.map
|
|
406
|
+
//# sourceMappingURL=result-parser.js.map
|