@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.
Files changed (238) hide show
  1. package/dist/create-db-context.d.ts +10 -10
  2. package/dist/create-db-context.js +312 -276
  3. package/dist/create-db-context.js.map +1 -6
  4. package/dist/ddl/column-ddl.d.ts +4 -4
  5. package/dist/ddl/column-ddl.js +41 -35
  6. package/dist/ddl/column-ddl.js.map +1 -6
  7. package/dist/ddl/initialize.d.ts +17 -17
  8. package/dist/ddl/initialize.js +200 -142
  9. package/dist/ddl/initialize.js.map +1 -6
  10. package/dist/ddl/relation-ddl.d.ts +6 -6
  11. package/dist/ddl/relation-ddl.js +55 -48
  12. package/dist/ddl/relation-ddl.js.map +1 -6
  13. package/dist/ddl/schema-ddl.d.ts +4 -4
  14. package/dist/ddl/schema-ddl.js +21 -15
  15. package/dist/ddl/schema-ddl.js.map +1 -6
  16. package/dist/ddl/table-ddl.d.ts +20 -20
  17. package/dist/ddl/table-ddl.js +139 -93
  18. package/dist/ddl/table-ddl.js.map +1 -6
  19. package/dist/define-db-context.js +10 -13
  20. package/dist/define-db-context.js.map +1 -6
  21. package/dist/errors/db-transaction-error.d.ts +15 -15
  22. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  23. package/dist/errors/db-transaction-error.js +53 -19
  24. package/dist/errors/db-transaction-error.js.map +1 -6
  25. package/dist/exec/executable.d.ts +23 -23
  26. package/dist/exec/executable.js +94 -40
  27. package/dist/exec/executable.js.map +1 -6
  28. package/dist/exec/queryable.d.ts +97 -97
  29. package/dist/exec/queryable.js +1310 -1204
  30. package/dist/exec/queryable.js.map +1 -6
  31. package/dist/exec/search-parser.d.ts +31 -31
  32. package/dist/exec/search-parser.d.ts.map +1 -1
  33. package/dist/exec/search-parser.js +158 -59
  34. package/dist/exec/search-parser.js.map +1 -6
  35. package/dist/expr/expr-unit.d.ts +4 -4
  36. package/dist/expr/expr-unit.js +24 -18
  37. package/dist/expr/expr-unit.js.map +1 -6
  38. package/dist/expr/expr.d.ts +6 -6
  39. package/dist/expr/expr.js +1872 -1844
  40. package/dist/expr/expr.js.map +1 -6
  41. package/dist/index.js +23 -1
  42. package/dist/index.js.map +1 -6
  43. package/dist/models/system-migration.js +7 -7
  44. package/dist/models/system-migration.js.map +1 -6
  45. package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
  46. package/dist/query-builder/base/expr-renderer-base.js +27 -21
  47. package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
  48. package/dist/query-builder/base/query-builder-base.d.ts +21 -21
  49. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  50. package/dist/query-builder/base/query-builder-base.js +90 -80
  51. package/dist/query-builder/base/query-builder-base.js.map +1 -6
  52. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  53. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  54. package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
  55. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
  56. package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
  57. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
  58. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  59. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  60. package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
  61. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
  62. package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
  63. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
  64. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  65. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  66. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
  67. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
  68. package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
  69. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
  70. package/dist/query-builder/query-builder.d.ts +1 -1
  71. package/dist/query-builder/query-builder.js +13 -13
  72. package/dist/query-builder/query-builder.js.map +1 -6
  73. package/dist/schema/factory/column-builder.d.ts +84 -84
  74. package/dist/schema/factory/column-builder.js +248 -185
  75. package/dist/schema/factory/column-builder.js.map +1 -6
  76. package/dist/schema/factory/index-builder.d.ts +38 -38
  77. package/dist/schema/factory/index-builder.js +144 -85
  78. package/dist/schema/factory/index-builder.js.map +1 -6
  79. package/dist/schema/factory/relation-builder.d.ts +91 -91
  80. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  81. package/dist/schema/factory/relation-builder.js +274 -136
  82. package/dist/schema/factory/relation-builder.js.map +1 -6
  83. package/dist/schema/procedure-builder.d.ts +51 -51
  84. package/dist/schema/procedure-builder.d.ts.map +1 -1
  85. package/dist/schema/procedure-builder.js +205 -131
  86. package/dist/schema/procedure-builder.js.map +1 -6
  87. package/dist/schema/table-builder.d.ts +55 -55
  88. package/dist/schema/table-builder.d.ts.map +1 -1
  89. package/dist/schema/table-builder.js +274 -205
  90. package/dist/schema/table-builder.js.map +1 -6
  91. package/dist/schema/view-builder.d.ts +44 -44
  92. package/dist/schema/view-builder.d.ts.map +1 -1
  93. package/dist/schema/view-builder.js +189 -116
  94. package/dist/schema/view-builder.js.map +1 -6
  95. package/dist/types/column.js +60 -30
  96. package/dist/types/column.js.map +1 -6
  97. package/dist/types/db-context-def.d.ts +9 -9
  98. package/dist/types/db-context-def.js +2 -1
  99. package/dist/types/db-context-def.js.map +1 -6
  100. package/dist/types/db.d.ts +47 -47
  101. package/dist/types/db.js +15 -5
  102. package/dist/types/db.js.map +1 -6
  103. package/dist/types/expr.d.ts +81 -81
  104. package/dist/types/expr.d.ts.map +1 -1
  105. package/dist/types/expr.js +3 -1
  106. package/dist/types/expr.js.map +1 -6
  107. package/dist/types/query-def.d.ts +46 -46
  108. package/dist/types/query-def.d.ts.map +1 -1
  109. package/dist/types/query-def.js +31 -24
  110. package/dist/types/query-def.js.map +1 -6
  111. package/dist/utils/result-parser.js +362 -221
  112. package/dist/utils/result-parser.js.map +1 -6
  113. package/package.json +5 -7
  114. package/src/create-db-context.ts +31 -31
  115. package/src/ddl/column-ddl.ts +4 -4
  116. package/src/ddl/initialize.ts +38 -38
  117. package/src/ddl/relation-ddl.ts +6 -6
  118. package/src/ddl/schema-ddl.ts +4 -4
  119. package/src/ddl/table-ddl.ts +24 -24
  120. package/src/errors/db-transaction-error.ts +13 -13
  121. package/src/exec/executable.ts +25 -25
  122. package/src/exec/queryable.ts +134 -134
  123. package/src/exec/search-parser.ts +50 -50
  124. package/src/expr/expr-unit.ts +4 -4
  125. package/src/expr/expr.ts +13 -13
  126. package/src/index.ts +8 -8
  127. package/src/models/system-migration.ts +1 -1
  128. package/src/query-builder/base/expr-renderer-base.ts +21 -21
  129. package/src/query-builder/base/query-builder-base.ts +33 -33
  130. package/src/query-builder/mssql/mssql-expr-renderer.ts +11 -11
  131. package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
  132. package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
  133. package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
  134. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
  135. package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
  136. package/src/query-builder/query-builder.ts +1 -1
  137. package/src/schema/factory/column-builder.ts +86 -86
  138. package/src/schema/factory/index-builder.ts +38 -38
  139. package/src/schema/factory/relation-builder.ts +93 -93
  140. package/src/schema/procedure-builder.ts +52 -52
  141. package/src/schema/table-builder.ts +56 -56
  142. package/src/schema/view-builder.ts +45 -45
  143. package/src/types/column.ts +1 -1
  144. package/src/types/db-context-def.ts +15 -15
  145. package/src/types/db.ts +50 -50
  146. package/src/types/expr.ts +103 -103
  147. package/src/types/query-def.ts +50 -50
  148. package/src/utils/result-parser.ts +39 -39
  149. package/README.md +0 -192
  150. package/docs/core.md +0 -234
  151. package/docs/expression.md +0 -234
  152. package/docs/query-builder.md +0 -93
  153. package/docs/queryable.md +0 -198
  154. package/docs/schema-builders.md +0 -463
  155. package/docs/types.md +0 -445
  156. package/docs/utilities.md +0 -27
  157. package/tests/db-context/create-db-context.spec.ts +0 -193
  158. package/tests/db-context/define-db-context.spec.ts +0 -17
  159. package/tests/ddl/basic.expected.ts +0 -341
  160. package/tests/ddl/basic.spec.ts +0 -557
  161. package/tests/ddl/column-builder.expected.ts +0 -310
  162. package/tests/ddl/column-builder.spec.ts +0 -525
  163. package/tests/ddl/index-builder.expected.ts +0 -38
  164. package/tests/ddl/index-builder.spec.ts +0 -148
  165. package/tests/ddl/procedure-builder.expected.ts +0 -52
  166. package/tests/ddl/procedure-builder.spec.ts +0 -128
  167. package/tests/ddl/relation-builder.expected.ts +0 -36
  168. package/tests/ddl/relation-builder.spec.ts +0 -171
  169. package/tests/ddl/table-builder.expected.ts +0 -113
  170. package/tests/ddl/table-builder.spec.ts +0 -399
  171. package/tests/ddl/view-builder.expected.ts +0 -38
  172. package/tests/ddl/view-builder.spec.ts +0 -116
  173. package/tests/dml/delete.expected.ts +0 -96
  174. package/tests/dml/delete.spec.ts +0 -127
  175. package/tests/dml/insert.expected.ts +0 -192
  176. package/tests/dml/insert.spec.ts +0 -210
  177. package/tests/dml/update.expected.ts +0 -176
  178. package/tests/dml/update.spec.ts +0 -222
  179. package/tests/dml/upsert.expected.ts +0 -215
  180. package/tests/dml/upsert.spec.ts +0 -190
  181. package/tests/errors/queryable-errors.spec.ts +0 -126
  182. package/tests/escape.spec.ts +0 -59
  183. package/tests/examples/pivot.expected.ts +0 -211
  184. package/tests/examples/pivot.spec.ts +0 -200
  185. package/tests/examples/sampling.expected.ts +0 -69
  186. package/tests/examples/sampling.spec.ts +0 -42
  187. package/tests/examples/unpivot.expected.ts +0 -120
  188. package/tests/examples/unpivot.spec.ts +0 -161
  189. package/tests/exec/search-parser.spec.ts +0 -267
  190. package/tests/executable/basic.expected.ts +0 -18
  191. package/tests/executable/basic.spec.ts +0 -54
  192. package/tests/expr/comparison.expected.ts +0 -282
  193. package/tests/expr/comparison.spec.ts +0 -334
  194. package/tests/expr/conditional.expected.ts +0 -134
  195. package/tests/expr/conditional.spec.ts +0 -249
  196. package/tests/expr/date.expected.ts +0 -332
  197. package/tests/expr/date.spec.ts +0 -459
  198. package/tests/expr/math.expected.ts +0 -62
  199. package/tests/expr/math.spec.ts +0 -59
  200. package/tests/expr/string.expected.ts +0 -218
  201. package/tests/expr/string.spec.ts +0 -300
  202. package/tests/expr/utility.expected.ts +0 -147
  203. package/tests/expr/utility.spec.ts +0 -155
  204. package/tests/select/basic.expected.ts +0 -322
  205. package/tests/select/basic.spec.ts +0 -433
  206. package/tests/select/filter.expected.ts +0 -357
  207. package/tests/select/filter.spec.ts +0 -954
  208. package/tests/select/group.expected.ts +0 -169
  209. package/tests/select/group.spec.ts +0 -159
  210. package/tests/select/join.expected.ts +0 -582
  211. package/tests/select/join.spec.ts +0 -692
  212. package/tests/select/order.expected.ts +0 -150
  213. package/tests/select/order.spec.ts +0 -140
  214. package/tests/select/recursive-cte.expected.ts +0 -244
  215. package/tests/select/recursive-cte.spec.ts +0 -514
  216. package/tests/select/result-meta.spec.ts +0 -270
  217. package/tests/select/subquery.expected.ts +0 -363
  218. package/tests/select/subquery.spec.ts +0 -441
  219. package/tests/select/view.expected.ts +0 -155
  220. package/tests/select/view.spec.ts +0 -235
  221. package/tests/select/window.expected.ts +0 -345
  222. package/tests/select/window.spec.ts +0 -433
  223. package/tests/setup/MockExecutor.ts +0 -18
  224. package/tests/setup/TestDbContext.ts +0 -59
  225. package/tests/setup/models/Company.ts +0 -13
  226. package/tests/setup/models/Employee.ts +0 -10
  227. package/tests/setup/models/MonthlySales.ts +0 -11
  228. package/tests/setup/models/Post.ts +0 -16
  229. package/tests/setup/models/Sales.ts +0 -10
  230. package/tests/setup/models/User.ts +0 -19
  231. package/tests/setup/procedure/GetAllUsers.ts +0 -9
  232. package/tests/setup/procedure/GetUserById.ts +0 -12
  233. package/tests/setup/test-utils.ts +0 -72
  234. package/tests/setup/views/ActiveUsers.ts +0 -8
  235. package/tests/setup/views/UserSummary.ts +0 -11
  236. package/tests/types/nullable-queryable-record.spec.ts +0 -97
  237. package/tests/utils/result-parser-perf.spec.ts +0 -143
  238. 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
- if (value == null) {
4
- return void 0;
5
- }
6
- switch (type) {
7
- case "number": {
8
- const num = Number(value);
9
- if (Number.isNaN(num)) {
10
- throw new Error(`Failed to parse number: ${String(value)}`);
11
- }
12
- return num;
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
- return Object.entries(columns).map(([key, type]) => ({
37
- key,
38
- type,
39
- parts: key.includes(".") ? key.split(".") : void 0
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
- const result = {};
44
- for (const { key, type, parts } of columnInfos) {
45
- const rawValue = record[key];
46
- const parsedValue = parseValue(rawValue, type);
47
- if (parsedValue === void 0) continue;
48
- if (parts != null) {
49
- let current = result;
50
- for (let i = 0; i < parts.length - 1; i++) {
51
- const part = parts[i];
52
- if (current[part] == null) {
53
- current[part] = {};
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
- return Object.keys(record).length === 0;
98
+ return Object.keys(record).length === 0;
66
99
  }
100
+ // ============================================
101
+ // Main Function
102
+ // ============================================
103
+ /** 양보 간격: N개 레코드마다 이벤트 루프에 양보 */
67
104
  const YIELD_INTERVAL = 100;
68
- const yieldToEventLoop = typeof setImmediate !== "undefined" ? () => new Promise((resolve) => setImmediate(resolve)) : () => new Promise((resolve) => setTimeout(resolve, 0));
69
- async function parseQueryResult(rawResults, meta) {
70
- if (rawResults.length === 0) {
71
- return void 0;
72
- }
73
- const joinKeys = Object.keys(meta.joins);
74
- if (joinKeys.length === 0) {
75
- return parseSimpleRecords(rawResults, meta.columns);
76
- }
77
- return parseJoinedRecords(rawResults, meta);
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
- const columnInfos = buildColumnInfos(columns);
81
- const results = [];
82
- for (let i = 0; i < rawResults.length; i++) {
83
- if (i > 0 && i % YIELD_INTERVAL === 0) {
84
- await yieldToEventLoop();
85
- }
86
- const parsed = flatToNested(rawResults[i], columnInfos);
87
- if (!isEmptyObject(parsed)) {
88
- results.push(parsed);
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
- return results.length > 0 ? results : void 0;
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
- return [...joinKeys].sort((a, b) => {
95
- const depthA = a.split(".").length;
96
- const depthB = b.split(".").length;
97
- return depthA - depthB;
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
- const columnInfos = buildColumnInfos(meta.columns);
102
- const nestedRecords = [];
103
- for (let i = 0; i < rawResults.length; i++) {
104
- if (i > 0 && i % YIELD_INTERVAL === 0) {
105
- await yieldToEventLoop();
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
- nestedRecords.push(flatToNested(rawResults[i], columnInfos));
108
- }
109
- const sortedJoinKeys = sortJoinKeysByDepth(Object.keys(meta.joins));
110
- const results = groupRecordsRecursively(nestedRecords, sortedJoinKeys, meta.joins, "");
111
- const filteredResults = results.filter((r) => !isEmptyObject(r));
112
- return filteredResults.length > 0 ? filteredResults : void 0;
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
- const keys = cachedKeyOrder ?? Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
116
- let result = "";
117
- for (let i = 0; i < keys.length; i++) {
118
- if (i > 0) result += "|";
119
- const v = groupKey[keys[i]];
120
- result += keys[i];
121
- result += ":";
122
- result += v === null ? "null" : String(v);
123
- }
124
- return result;
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
- const childJoinKeys = allJoinKeys.filter((key) => {
128
- if (currentPath === "") {
129
- return !key.includes(".");
130
- } else {
131
- return key.startsWith(currentPath + ".") && key.slice(currentPath.length + 1).indexOf(".") === -1;
132
- }
133
- });
134
- if (childJoinKeys.length === 0) {
135
- return records;
136
- }
137
- const groupMap = /* @__PURE__ */ new Map();
138
- const joinKeyExclusions = buildJoinKeyExclusionSet(childJoinKeys);
139
- let groupKeyOrder;
140
- for (const record of records) {
141
- const groupKey = extractGroupKey(record, joinKeyExclusions);
142
- if (groupKeyOrder == null) {
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
- const keyStr = serializeGroupKey(groupKey, groupKeyOrder);
146
- const existingGroup = groupMap.get(keyStr);
147
- if (existingGroup != null) {
148
- for (const joinKey of childJoinKeys) {
149
- const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
150
- mergeJoinData(existingGroup, record, localKey, joinsConfig[joinKey].isSingle);
151
- }
152
- } else {
153
- const newGroup = { ...record };
154
- for (const joinKey of childJoinKeys) {
155
- const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
156
- const joinData = newGroup[localKey];
157
- if (joinData != null && !isEmptyObject(joinData)) {
158
- if (!joinsConfig[joinKey].isSingle) {
159
- newGroup[localKey] = [joinData];
160
- }
161
- } else {
162
- delete newGroup[localKey];
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
- const grouped = Array.from(groupMap.values());
169
- for (const group of grouped) {
170
- for (const joinKey of childJoinKeys) {
171
- const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
172
- const joinData = group[localKey];
173
- if (Array.isArray(joinData) && joinData.length > 0) {
174
- group[localKey] = groupRecordsRecursively(
175
- joinData,
176
- allJoinKeys,
177
- joinsConfig,
178
- joinKey
179
- );
180
- } else if (joinData != null && typeof joinData === "object" && !Array.isArray(joinData)) {
181
- const processed = groupRecordsRecursively(
182
- [joinData],
183
- allJoinKeys,
184
- joinsConfig,
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
- for (const group of grouped) {
194
- for (const key of Object.keys(group)) {
195
- if (key.startsWith("__hashSet__")) {
196
- delete group[key];
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
- const exclusions = /* @__PURE__ */ new Set();
204
- for (const jk of joinKeys) {
205
- exclusions.add(jk);
206
- const parts = jk.split(".");
207
- for (let i = 1; i < parts.length; i++) {
208
- exclusions.add(parts.slice(0, i).join("."));
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
- const result = {};
215
- for (const [key, value] of Object.entries(record)) {
216
- if (!joinKeyExclusions.has(key)) {
217
- if (value == null || typeof value !== "object") {
218
- result[key] = value;
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
- const newJoinData = newRecord[localKey];
226
- if (newJoinData == null || isEmptyObject(newJoinData)) {
227
- return;
228
- }
229
- const existingJoinData = existingGroup[localKey];
230
- if (isSingle) {
231
- if (existingJoinData != null) {
232
- if (!obj.equal(existingJoinData, newJoinData)) {
233
- throw new Error(`isSingle relationship '${localKey}' has multiple different results.`);
234
- }
235
- } else {
236
- existingGroup[localKey] = newJoinData;
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
- } else {
239
- const hashSetKey = `__hashSet__${localKey}`;
240
- if (!Array.isArray(existingJoinData)) {
241
- existingGroup[localKey] = [newJoinData];
242
- existingGroup[hashSetKey] = /* @__PURE__ */ new Set([serializeGroupKey(newJoinData)]);
243
- } else {
244
- const hashSet = existingGroup[hashSetKey];
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
- } else {
252
- const isDuplicate = existingJoinData.some(
253
- (item) => obj.equal(item, newJoinData)
254
- );
255
- if (!isDuplicate) {
256
- existingJoinData.push(newJoinData);
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
- export {
263
- parseQueryResult
264
- };
265
- //# sourceMappingURL=result-parser.js.map
406
+ //# sourceMappingURL=result-parser.js.map