@simplysm/orm-common 13.0.99 → 14.0.1
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/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 +6 -6
- 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 +4 -4
- 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.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 +4 -4
- 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.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 +4 -4
- 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.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 +91 -91
- 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.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.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/package.json +5 -7
- 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 +134 -134
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +13 -13
- 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 +11 -11
- package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
- package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
- package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
- package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
- 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 +93 -93
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +45 -45
- package/src/types/column.ts +1 -1
- 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 +39 -39
- package/README.md +0 -192
- package/docs/core.md +0 -234
- package/docs/expression.md +0 -234
- package/docs/query-builder.md +0 -93
- package/docs/queryable.md +0 -198
- package/docs/schema-builders.md +0 -463
- package/docs/types.md +0 -445
- 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
|
+
// Type Parsers
|
|
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
|
+
// Main Function
|
|
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
|
+
* Transform DB query result to TypeScript object via ResultMeta
|
|
111
|
+
*
|
|
112
|
+
* @param rawResults - Raw result array from database
|
|
113
|
+
* @param meta - Type transformation and JOIN structure information (required)
|
|
114
|
+
* @returns Type-transformed and nested result array. Returns undefined if input is empty or no valid results
|
|
115
|
+
* @throws Error if type parsing fails
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* - meta required: no need to call this function without meta (input = output)
|
|
119
|
+
* - async only: no synchronous version provided for large-scale processing to allow external interrupts
|
|
120
|
+
* - browser/node compatible: yields via setTimeout(resolve, 0)
|
|
121
|
+
* - empty result handling: returns undefined if input array is empty or all records are empty objects after parsing
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* // 1. Simple type parsing
|
|
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 result nesting
|
|
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
|
+
// Handle empty input
|
|
146
|
+
if (rawResults.length === 0) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const joinKeys = Object.keys(meta.joins);
|
|
150
|
+
// No JOINs: simple type parsing only
|
|
151
|
+
if (joinKeys.length === 0) {
|
|
152
|
+
return parseSimpleRecords(rawResults, meta.columns);
|
|
153
|
+
}
|
|
154
|
+
// With JOINs: grouping + nesting
|
|
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. Transform all records to nested structure
|
|
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. Sort JOIN keys by depth (shallower ones first)
|
|
202
|
+
const sortedJoinKeys = sortJoinKeysByDepth(Object.keys(meta.joins));
|
|
203
|
+
// 3. Recursively group from root level
|
|
204
|
+
const results = groupRecordsRecursively(nestedRecords, sortedJoinKeys, meta.joins, "");
|
|
205
|
+
// 4. Filter empty results
|
|
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
|
+
* Recursively group records for current path
|
|
229
|
+
*
|
|
230
|
+
* Achieves O(n) complexity with Map-based grouping
|
|
231
|
+
*
|
|
232
|
+
* @param records - Record array to group
|
|
233
|
+
* @param allJoinKeys - All JOIN keys (sorted by depth)
|
|
234
|
+
* @param joinsConfig - JOIN configuration
|
|
235
|
+
* @param currentPath - Current path (e.g., "", "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
|
+
// Find JOIN keys directly corresponding to current path
|
|
239
|
+
// e.g., currentPath="" → ["posts", "company"]
|
|
240
|
+
// e.g., currentPath="posts" → ["posts.comments"]
|
|
241
|
+
const childJoinKeys = allJoinKeys.filter((key) => {
|
|
242
|
+
if (currentPath === "") {
|
|
243
|
+
// Root level: keys without dots
|
|
244
|
+
return !key.includes(".");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Sublevel: current path + "." + key
|
|
248
|
+
return (key.startsWith(currentPath + ".") && key.slice(currentPath.length + 1).indexOf(".") === -1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
if (childJoinKeys.length === 0) {
|
|
252
|
+
// No more JOINs to group
|
|
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-based grouping (O(n) complexity)
|
|
256
|
+
const groupMap = new Map();
|
|
257
|
+
// Precompute join key exclusion set for O(1) lookup
|
|
258
|
+
const joinKeyExclusions = buildJoinKeyExclusionSet(childJoinKeys);
|
|
259
|
+
// Key order caching (determined from first record and reused)
|
|
260
|
+
let groupKeyOrder;
|
|
261
|
+
for (const record of records) {
|
|
262
|
+
// Extract and serialize group key (excluding JOIN keys)
|
|
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
|
+
// Merge JOIN data to existing group
|
|
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
|
+
// Generate new group
|
|
278
|
+
const newGroup = { ...record };
|
|
279
|
+
// Initialize each JOIN key as array or single object
|
|
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
|
+
// Transform to array
|
|
286
|
+
newGroup[localKey] = [joinData];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Delete key if data is empty
|
|
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
|
+
// Transform Map to array
|
|
298
|
+
const grouped = Array.from(groupMap.values());
|
|
299
|
+
// Recursively process sublevel of each 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
|
+
// Array case: process sublevel recursively
|
|
306
|
+
group[localKey] = groupRecordsRecursively(joinData, allJoinKeys, joinsConfig, joinKey);
|
|
307
|
+
}
|
|
308
|
+
else if (joinData != null && typeof joinData === "object" && !Array.isArray(joinData)) {
|
|
309
|
+
// Single object case (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
|
+
// Remove __hashSet__ internal property (temporary property for duplicate checking)
|
|
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 - error if data exists and values differ
|
|
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 → Add to array
|
|
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
|