@simplysm/orm-common 13.0.68 → 13.0.70

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 (204) hide show
  1. package/README.md +54 -1447
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +9 -9
  4. package/dist/create-db-context.js.map +1 -1
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/initialize.d.ts +17 -17
  7. package/dist/ddl/initialize.js +2 -2
  8. package/dist/ddl/initialize.js.map +1 -1
  9. package/dist/ddl/relation-ddl.d.ts +6 -6
  10. package/dist/ddl/schema-ddl.d.ts +4 -4
  11. package/dist/ddl/table-ddl.d.ts +24 -24
  12. package/dist/ddl/table-ddl.js +4 -4
  13. package/dist/ddl/table-ddl.js.map +1 -1
  14. package/dist/errors/db-transaction-error.d.ts +15 -15
  15. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  16. package/dist/exec/executable.d.ts +23 -23
  17. package/dist/exec/executable.js +3 -3
  18. package/dist/exec/executable.js.map +1 -1
  19. package/dist/exec/queryable.d.ts +160 -160
  20. package/dist/exec/queryable.js +119 -119
  21. package/dist/exec/queryable.js.map +1 -1
  22. package/dist/exec/search-parser.d.ts +37 -37
  23. package/dist/exec/search-parser.d.ts.map +1 -1
  24. package/dist/expr/expr-unit.d.ts +4 -4
  25. package/dist/expr/expr.d.ts +257 -257
  26. package/dist/expr/expr.js +265 -265
  27. package/dist/expr/expr.js.map +1 -1
  28. package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
  29. package/dist/query-builder/base/expr-renderer-base.js +2 -2
  30. package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
  31. package/dist/query-builder/base/query-builder-base.d.ts +26 -26
  32. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  33. package/dist/query-builder/base/query-builder-base.js +22 -22
  34. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  35. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  36. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  37. package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
  38. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  39. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  40. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  41. package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
  42. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  43. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  44. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  45. package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
  46. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  47. package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
  48. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  49. package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
  50. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  51. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  52. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  53. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
  54. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  55. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
  56. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  57. package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
  58. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  59. package/dist/query-builder/query-builder.d.ts +1 -1
  60. package/dist/schema/factory/column-builder.d.ts +79 -79
  61. package/dist/schema/factory/column-builder.js +42 -42
  62. package/dist/schema/factory/index-builder.d.ts +39 -39
  63. package/dist/schema/factory/index-builder.js +26 -26
  64. package/dist/schema/factory/relation-builder.d.ts +99 -99
  65. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  66. package/dist/schema/factory/relation-builder.js +38 -38
  67. package/dist/schema/procedure-builder.d.ts +49 -49
  68. package/dist/schema/procedure-builder.d.ts.map +1 -1
  69. package/dist/schema/procedure-builder.js +33 -33
  70. package/dist/schema/table-builder.d.ts +59 -59
  71. package/dist/schema/table-builder.d.ts.map +1 -1
  72. package/dist/schema/table-builder.js +43 -43
  73. package/dist/schema/view-builder.d.ts +49 -49
  74. package/dist/schema/view-builder.d.ts.map +1 -1
  75. package/dist/schema/view-builder.js +32 -32
  76. package/dist/types/column.d.ts +22 -22
  77. package/dist/types/column.js +1 -1
  78. package/dist/types/column.js.map +1 -1
  79. package/dist/types/db.d.ts +40 -40
  80. package/dist/types/expr.d.ts +59 -59
  81. package/dist/types/expr.d.ts.map +1 -1
  82. package/dist/types/query-def.d.ts +44 -44
  83. package/dist/types/query-def.d.ts.map +1 -1
  84. package/dist/utils/result-parser.d.ts +11 -11
  85. package/dist/utils/result-parser.js +3 -3
  86. package/dist/utils/result-parser.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/create-db-context.ts +20 -20
  89. package/src/ddl/column-ddl.ts +4 -4
  90. package/src/ddl/initialize.ts +259 -259
  91. package/src/ddl/relation-ddl.ts +89 -89
  92. package/src/ddl/schema-ddl.ts +4 -4
  93. package/src/ddl/table-ddl.ts +189 -189
  94. package/src/errors/db-transaction-error.ts +13 -13
  95. package/src/exec/executable.ts +25 -25
  96. package/src/exec/queryable.ts +2033 -2033
  97. package/src/exec/search-parser.ts +57 -57
  98. package/src/expr/expr-unit.ts +4 -4
  99. package/src/expr/expr.ts +2140 -2140
  100. package/src/query-builder/base/expr-renderer-base.ts +237 -237
  101. package/src/query-builder/base/query-builder-base.ts +213 -213
  102. package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
  103. package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
  104. package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
  105. package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
  106. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
  107. package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
  108. package/src/query-builder/query-builder.ts +19 -19
  109. package/src/schema/factory/column-builder.ts +423 -423
  110. package/src/schema/factory/index-builder.ts +164 -164
  111. package/src/schema/factory/relation-builder.ts +453 -453
  112. package/src/schema/procedure-builder.ts +232 -232
  113. package/src/schema/table-builder.ts +319 -319
  114. package/src/schema/view-builder.ts +221 -221
  115. package/src/types/column.ts +188 -188
  116. package/src/types/db.ts +208 -208
  117. package/src/types/expr.ts +697 -697
  118. package/src/types/query-def.ts +513 -513
  119. package/src/utils/result-parser.ts +458 -458
  120. package/tests/db-context/create-db-context.spec.ts +224 -0
  121. package/tests/db-context/define-db-context.spec.ts +68 -0
  122. package/tests/ddl/basic.expected.ts +341 -0
  123. package/tests/ddl/basic.spec.ts +714 -0
  124. package/tests/ddl/column-builder.expected.ts +310 -0
  125. package/tests/ddl/column-builder.spec.ts +637 -0
  126. package/tests/ddl/index-builder.expected.ts +38 -0
  127. package/tests/ddl/index-builder.spec.ts +202 -0
  128. package/tests/ddl/procedure-builder.expected.ts +52 -0
  129. package/tests/ddl/procedure-builder.spec.ts +234 -0
  130. package/tests/ddl/relation-builder.expected.ts +36 -0
  131. package/tests/ddl/relation-builder.spec.ts +372 -0
  132. package/tests/ddl/table-builder.expected.ts +113 -0
  133. package/tests/ddl/table-builder.spec.ts +433 -0
  134. package/tests/ddl/view-builder.expected.ts +38 -0
  135. package/tests/ddl/view-builder.spec.ts +176 -0
  136. package/tests/dml/delete.expected.ts +96 -0
  137. package/tests/dml/delete.spec.ts +160 -0
  138. package/tests/dml/insert.expected.ts +192 -0
  139. package/tests/dml/insert.spec.ts +288 -0
  140. package/tests/dml/update.expected.ts +176 -0
  141. package/tests/dml/update.spec.ts +318 -0
  142. package/tests/dml/upsert.expected.ts +215 -0
  143. package/tests/dml/upsert.spec.ts +242 -0
  144. package/tests/errors/queryable-errors.spec.ts +177 -0
  145. package/tests/escape.spec.ts +100 -0
  146. package/tests/examples/pivot.expected.ts +211 -0
  147. package/tests/examples/pivot.spec.ts +533 -0
  148. package/tests/examples/sampling.expected.ts +69 -0
  149. package/tests/examples/sampling.spec.ts +104 -0
  150. package/tests/examples/unpivot.expected.ts +120 -0
  151. package/tests/examples/unpivot.spec.ts +226 -0
  152. package/tests/exec/search-parser.spec.ts +283 -0
  153. package/tests/executable/basic.expected.ts +18 -0
  154. package/tests/executable/basic.spec.ts +54 -0
  155. package/tests/expr/comparison.expected.ts +282 -0
  156. package/tests/expr/comparison.spec.ts +400 -0
  157. package/tests/expr/conditional.expected.ts +134 -0
  158. package/tests/expr/conditional.spec.ts +276 -0
  159. package/tests/expr/date.expected.ts +332 -0
  160. package/tests/expr/date.spec.ts +526 -0
  161. package/tests/expr/math.expected.ts +62 -0
  162. package/tests/expr/math.spec.ts +106 -0
  163. package/tests/expr/string.expected.ts +218 -0
  164. package/tests/expr/string.spec.ts +356 -0
  165. package/tests/expr/utility.expected.ts +147 -0
  166. package/tests/expr/utility.spec.ts +182 -0
  167. package/tests/select/basic.expected.ts +322 -0
  168. package/tests/select/basic.spec.ts +502 -0
  169. package/tests/select/filter.expected.ts +357 -0
  170. package/tests/select/filter.spec.ts +1068 -0
  171. package/tests/select/group.expected.ts +169 -0
  172. package/tests/select/group.spec.ts +244 -0
  173. package/tests/select/join.expected.ts +582 -0
  174. package/tests/select/join.spec.ts +805 -0
  175. package/tests/select/order.expected.ts +150 -0
  176. package/tests/select/order.spec.ts +189 -0
  177. package/tests/select/recursive-cte.expected.ts +244 -0
  178. package/tests/select/recursive-cte.spec.ts +514 -0
  179. package/tests/select/result-meta.spec.ts +270 -0
  180. package/tests/select/subquery.expected.ts +363 -0
  181. package/tests/select/subquery.spec.ts +537 -0
  182. package/tests/select/view.expected.ts +155 -0
  183. package/tests/select/view.spec.ts +235 -0
  184. package/tests/select/window.expected.ts +345 -0
  185. package/tests/select/window.spec.ts +618 -0
  186. package/tests/setup/MockExecutor.ts +18 -0
  187. package/tests/setup/TestDbContext.ts +59 -0
  188. package/tests/setup/models/Company.ts +13 -0
  189. package/tests/setup/models/Employee.ts +10 -0
  190. package/tests/setup/models/MonthlySales.ts +11 -0
  191. package/tests/setup/models/Post.ts +16 -0
  192. package/tests/setup/models/Sales.ts +10 -0
  193. package/tests/setup/models/User.ts +19 -0
  194. package/tests/setup/procedure/GetAllUsers.ts +9 -0
  195. package/tests/setup/procedure/GetUserById.ts +12 -0
  196. package/tests/setup/test-utils.ts +72 -0
  197. package/tests/setup/views/ActiveUsers.ts +8 -0
  198. package/tests/setup/views/UserSummary.ts +11 -0
  199. package/tests/types/nullable-queryable-record.spec.ts +145 -0
  200. package/tests/utils/result-parser-perf.spec.ts +210 -0
  201. package/tests/utils/result-parser.spec.ts +701 -0
  202. package/docs/expressions.md +0 -172
  203. package/docs/queries.md +0 -444
  204. package/docs/schema.md +0 -245
@@ -0,0 +1,10 @@
1
+ import { Table } from "../../../src/schema/table-builder";
2
+
3
+ export const Employee = Table("Employee")
4
+ .columns((c) => ({
5
+ id: c.bigint().autoIncrement(),
6
+ name: c.varchar(100),
7
+ managerId: c.bigint().nullable(), // 자기 참조 (상위 매니저)
8
+ departmentId: c.bigint().nullable(),
9
+ }))
10
+ .primaryKey("id");
@@ -0,0 +1,11 @@
1
+ import { Table } from "../../../src/schema/table-builder";
2
+
3
+ export const MonthlySales = Table("MonthlySales")
4
+ .columns((c) => ({
5
+ id: c.bigint().autoIncrement(),
6
+ category: c.varchar(50),
7
+ jan: c.int(),
8
+ feb: c.int(),
9
+ mar: c.int(),
10
+ }))
11
+ .primaryKey("id");
@@ -0,0 +1,16 @@
1
+ import { Table } from "../../../src/schema/table-builder";
2
+ import { User } from "./User";
3
+
4
+ export const Post = Table("Post")
5
+ .columns((c) => ({
6
+ id: c.bigint().autoIncrement(),
7
+ userId: c.bigint(),
8
+ title: c.varchar(300),
9
+ content: c.text().nullable(),
10
+ viewCount: c.int().default(0),
11
+ publishedAt: c.datetime().nullable(),
12
+ }))
13
+ .primaryKey("id")
14
+ .relations((r) => ({
15
+ user: r.foreignKey(["userId"], () => User),
16
+ }));
@@ -0,0 +1,10 @@
1
+ import { Table } from "../../../src/schema/table-builder";
2
+
3
+ export const Sales = Table("Sales")
4
+ .columns((c) => ({
5
+ id: c.bigint().autoIncrement(),
6
+ category: c.varchar(50),
7
+ year: c.int(),
8
+ amount: c.int(),
9
+ }))
10
+ .primaryKey("id");
@@ -0,0 +1,19 @@
1
+ import { Table } from "../../../src/schema/table-builder";
2
+ import { Post } from "./Post";
3
+ import { Company } from "./Company";
4
+
5
+ export const User = Table("User")
6
+ .columns((c) => ({
7
+ id: c.bigint().autoIncrement(),
8
+ name: c.varchar(100),
9
+ email: c.varchar(200).nullable(),
10
+ age: c.int().nullable(),
11
+ isActive: c.boolean().default(true),
12
+ companyId: c.bigint().nullable(),
13
+ createdAt: c.datetime(),
14
+ }))
15
+ .primaryKey("id")
16
+ .relations((r) => ({
17
+ company: r.foreignKey(["companyId"], () => Company),
18
+ posts: r.foreignKeyTarget(() => Post, "user"),
19
+ }));
@@ -0,0 +1,9 @@
1
+ import { Procedure } from "../../../src/schema/procedure-builder";
2
+
3
+ export const GetAllUsers = Procedure("GetAllUsers")
4
+ .returns((c) => ({
5
+ id: c.bigint(),
6
+ name: c.varchar(100),
7
+ email: c.varchar(200).nullable(),
8
+ }))
9
+ .body("-- DBMSwrite matching query --");
@@ -0,0 +1,12 @@
1
+ import { Procedure } from "../../../src/schema/procedure-builder";
2
+
3
+ export const GetUserById = Procedure("GetUserById")
4
+ .params((c) => ({
5
+ userId: c.bigint(),
6
+ }))
7
+ .returns((c) => ({
8
+ id: c.bigint(),
9
+ name: c.varchar(100),
10
+ email: c.varchar(200).nullable(),
11
+ }))
12
+ .body("-- DBMSwrite matching query --");
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Test utilities
3
+ * - toMatchSql matcher
4
+ * - dialects constant
5
+ * - ExpectedSql type
6
+ */
7
+ import { expect } from "vitest";
8
+ import type { Dialect, QueryBuildResult } from "../../src/types/db";
9
+
10
+ // ============================================
11
+ // Dialect list
12
+ // ============================================
13
+
14
+ export const dialects: Dialect[] = ["mysql", "mssql", "postgresql"];
15
+
16
+ // ============================================
17
+ // Expected SQL type
18
+ // ============================================
19
+
20
+ export type ExpectedSql = Record<Dialect, string>;
21
+
22
+ // ============================================
23
+ // SQL normalization (ignore whitespace/empty lines, normalize dynamic names)
24
+ // ============================================
25
+
26
+ function normalizeSql(sql: string): string {
27
+ return (
28
+ sql
29
+ // Completely remove all whitespace (spaces, tabs, newlines)
30
+ .replace(/\s+/g, "")
31
+ // Procedure name normalization (SD + 32-char hex) <-- now using multistatement instead of PROC
32
+ // .replace(/`SD[a-f0-9]{32}`/g, "`SD_PROC`")
33
+ // .replace(/\[#SD[a-f0-9]{32}]/g, "[#SD_PROC]")
34
+ // .replace(/\[SD[a-f0-9]{32}]/g, "[SD_PROC]")
35
+ // .replace(/"SD[a-f0-9]{32}"/g, '"SD_PROC"')
36
+ // Temporary table name normalization (SD_TEMP_ + 32-char hex)
37
+ .replace(/`SD_TEMP_[a-f0-9]{32}`/g, "`SD_TEMP`")
38
+ .replace(/\[SD_TEMP_[a-f0-9]{32}]/g, "[SD_TEMP]")
39
+ .replace(/"SD_TEMP_[a-f0-9]{32}"/g, '"SD_TEMP"')
40
+ );
41
+ }
42
+
43
+ // ============================================
44
+ // Custom matcher
45
+ // ============================================
46
+
47
+ expect.extend({
48
+ toMatchSql(received: string | QueryBuildResult, expected: string) {
49
+ const sql = typeof received === "string" ? received : received.sql;
50
+ const normalizedReceived = normalizeSql(sql);
51
+ const normalizedExpected = normalizeSql(expected);
52
+
53
+ const pass = normalizedReceived === normalizedExpected;
54
+
55
+ return {
56
+ pass,
57
+ actual: normalizedReceived,
58
+ expected: normalizedExpected,
59
+ message: () => (pass ? "SQL matches" : "SQL does not match"),
60
+ };
61
+ },
62
+ });
63
+
64
+ // Type declaration
65
+ declare module "vitest" {
66
+ interface Assertion<T> {
67
+ toMatchSql(expected: string): T;
68
+ }
69
+ interface AsymmetricMatchersContaining {
70
+ toMatchSql(expected: string): unknown;
71
+ }
72
+ }
@@ -0,0 +1,8 @@
1
+ import { View } from "../../../src/schema/view-builder";
2
+ import { expr } from "../../../src/expr/expr";
3
+ import type { TestDbTablesContext } from "../TestDbContext";
4
+
5
+ // 활성 사용자만 보여주는 뷰
6
+ export const ActiveUsers = View("ActiveUsers").query((db: TestDbTablesContext) =>
7
+ db.user().where((u) => [expr.eq(u.isActive, true)]),
8
+ );
@@ -0,0 +1,11 @@
1
+ import { View } from "../../../src/schema/view-builder";
2
+ import type { TestDbTablesContext } from "../TestDbContext";
3
+
4
+ // 사용자 요약 정보 뷰 (select 포함)
5
+ export const UserSummary = View("UserSummary").query((db: TestDbTablesContext) =>
6
+ db.user().select((u) => ({
7
+ id: u.id,
8
+ name: u.name,
9
+ email: u.email,
10
+ })),
11
+ );
@@ -0,0 +1,145 @@
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+ import { createTestDb } from "../setup/TestDbContext";
3
+ import { expr } from "../../src/expr/expr";
4
+ import { User } from "../setup/models/User";
5
+ import type {
6
+ NullableQueryableRecord,
7
+ QueryableRecord,
8
+ UnwrapQueryableRecord,
9
+ } from "../../src/exec/queryable";
10
+
11
+ describe("NullableQueryableRecord type inference", () => {
12
+ it("optional relation (joinSingle) fields should be ExprUnit<T | undefined>", () => {
13
+ const db = createTestDb();
14
+ const q = db
15
+ .post()
16
+ .joinSingle("user", (qr, c) => qr.from(User).where((u) => [expr.eq(u.id, c.userId)]))
17
+ .select((item) => ({
18
+ title: item.title,
19
+ userName: item.user!.name,
20
+ }));
21
+
22
+ // title is from the main table — should remain non-nullable
23
+ type Result = typeof q extends { meta: { columns: infer C } } ? C : never;
24
+ type TitleType = Result extends { title: { $infer: infer T } } ? T : never;
25
+ type UserNameType = Result extends { userName: { $infer: infer T } } ? T : never;
26
+
27
+ expectTypeOf<TitleType>().toEqualTypeOf<string>();
28
+ expectTypeOf<UserNameType>().toEqualTypeOf<string | undefined>();
29
+
30
+ // Runtime: query builds without error
31
+ expect(q).toBeDefined();
32
+ });
33
+
34
+ it("non-optional relation (required object) fields should remain ExprUnit<T>", () => {
35
+ const db = createTestDb();
36
+ const q = db.post().select((item) => ({
37
+ title: item.title,
38
+ viewCount: item.viewCount,
39
+ }));
40
+
41
+ type Result = typeof q extends { meta: { columns: infer C } } ? C : never;
42
+ type TitleType = Result extends { title: { $infer: infer T } } ? T : never;
43
+
44
+ expectTypeOf<TitleType>().toEqualTypeOf<string>();
45
+
46
+ expect(q).toBeDefined();
47
+ });
48
+
49
+ it("QueryableRecord preserves optional modifier (for select output)", () => {
50
+ type OptionalData = { id?: number; name: string };
51
+ type Result = QueryableRecord<OptionalData>;
52
+
53
+ // Key is optional — Required<> needed to access id
54
+ expectTypeOf<Required<Result>["id"]>().toMatchTypeOf<{ $infer: number | undefined }>();
55
+ expectTypeOf<Result["name"]>().toMatchTypeOf<{ $infer: string }>();
56
+
57
+ expect(true).toBe(true);
58
+ });
59
+
60
+ it("QueryableRecord preserves optional modifier for write operations", () => {
61
+ type OptionalData = { id?: number; name: string };
62
+ type WriteResult = QueryableRecord<OptionalData>;
63
+
64
+ // QueryableRecord preserves optional keys (for update/insert operations)
65
+ expectTypeOf<Required<WriteResult>["id"]>().toMatchTypeOf<{ $infer: number | undefined }>();
66
+ expectTypeOf<WriteResult["name"]>().toMatchTypeOf<{ $infer: string }>();
67
+
68
+ expect(true).toBe(true);
69
+ });
70
+
71
+ it("select auto-infers result type from callback return", () => {
72
+ const db = createTestDb();
73
+
74
+ const q = db
75
+ .user()
76
+ .where((u) => [expr.eq(u.isActive, true)])
77
+ .select((c) => ({
78
+ id: c.id,
79
+ name: c.name,
80
+ email: c.email,
81
+ isActive: c.isActive,
82
+ }));
83
+
84
+ // UnwrapQueryableRecord extracts primitive types from ExprUnit
85
+ type Result = typeof q extends { meta: { columns: infer C } } ? C : never;
86
+ type IdType = Result extends { id: { $infer: infer T } } ? T : never;
87
+ type EmailType = Result extends { email: { $infer: infer T } } ? T : never;
88
+
89
+ expectTypeOf<IdType>().toEqualTypeOf<number>();
90
+ expectTypeOf<EmailType>().toEqualTypeOf<string | undefined>();
91
+
92
+ expect(q).toBeDefined();
93
+ });
94
+
95
+ it("UnwrapQueryableRecord unwraps ExprUnit to primitive", () => {
96
+ type Input = {
97
+ id: import("../../src/expr/expr-unit").ExprUnit<number>;
98
+ name: import("../../src/expr/expr-unit").ExprUnit<string>;
99
+ email: import("../../src/expr/expr-unit").ExprUnit<string | undefined>;
100
+ };
101
+ type Result = UnwrapQueryableRecord<Input>;
102
+
103
+ expectTypeOf<Result>().toEqualTypeOf<{
104
+ id: number;
105
+ name: string;
106
+ email: string | undefined;
107
+ }>();
108
+
109
+ expect(true).toBe(true);
110
+ });
111
+
112
+ it("select with optional properties then orderBy should compile", () => {
113
+ const db = createTestDb();
114
+
115
+ /*type IUserItem = {
116
+ id?: number;
117
+ name?: string;
118
+ isActive: boolean;
119
+ };*/
120
+
121
+ const q = db
122
+ .user()
123
+ .where((u) => [expr.eq(u.isActive, true)])
124
+ .select((c) => ({
125
+ id: c.id,
126
+ name: c.name,
127
+ isActive: c.isActive,
128
+ }))
129
+ .orderBy((c) => c.id, "DESC");
130
+
131
+ expect(q).toBeDefined();
132
+ });
133
+
134
+ it("NullableQueryableRecord wraps primitives with | undefined", () => {
135
+ // Type-only test — verified by pnpm typecheck
136
+ type TestData = { name: string; age: number | undefined };
137
+ type Result = NullableQueryableRecord<TestData>;
138
+
139
+ // All primitives get | undefined in NullableQueryableRecord
140
+ expectTypeOf<Result["name"]>().toMatchTypeOf<{ $infer: string | undefined }>();
141
+ expectTypeOf<Result["age"]>().toMatchTypeOf<{ $infer: number | undefined }>();
142
+
143
+ expect(true).toBe(true);
144
+ });
145
+ });
@@ -0,0 +1,210 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseQueryResult } from "../../src/utils/result-parser";
3
+ import type { ResultMeta } from "../../src/types/db";
4
+
5
+ /**
6
+ * 성능 벤치마크 테스트
7
+ *
8
+ * 목적: objClone 제거 최적화 효과 측정
9
+ * 측정 항목:
10
+ * - 대용량 단순 레코드 processing 시간
11
+ * - 대용량 JOIN 레코드 processing 시간
12
+ * - 중첩 JOIN processing 시간
13
+ */
14
+ describe("result-parser performance", () => {
15
+ // 테스트 데이터 생성 헬퍼
16
+ function generateSimpleRecords(count: number): Record<string, unknown>[] {
17
+ return Array.from({ length: count }, (_, i) => ({
18
+ id: String(i + 1),
19
+ name: `User${i + 1}`,
20
+ email: `user${i + 1}@example.com`,
21
+ age: String(20 + (i % 50)),
22
+ active: i % 2,
23
+ }));
24
+ }
25
+
26
+ function generateJoinRecords(userCount: number, postsPerUser: number): Record<string, unknown>[] {
27
+ const records: Record<string, unknown>[] = [];
28
+ for (let u = 0; u < userCount; u++) {
29
+ for (let p = 0; p < postsPerUser; p++) {
30
+ records.push({
31
+ "id": String(u + 1),
32
+ "name": `User${u + 1}`,
33
+ "posts.id": String(u * postsPerUser + p + 1),
34
+ "posts.title": `Post${p + 1} by User${u + 1}`,
35
+ "posts.content": `Content of post ${p + 1}`,
36
+ });
37
+ }
38
+ }
39
+ return records;
40
+ }
41
+
42
+ function generateNestedJoinRecords(
43
+ userCount: number,
44
+ postsPerUser: number,
45
+ commentsPerPost: number,
46
+ ): Record<string, unknown>[] {
47
+ const records: Record<string, unknown>[] = [];
48
+ for (let u = 0; u < userCount; u++) {
49
+ for (let p = 0; p < postsPerUser; p++) {
50
+ for (let c = 0; c < commentsPerPost; c++) {
51
+ records.push({
52
+ "id": String(u + 1),
53
+ "name": `User${u + 1}`,
54
+ "posts.id": String(u * postsPerUser + p + 1),
55
+ "posts.title": `Post${p + 1}`,
56
+ "posts.comments.id": String((u * postsPerUser + p) * commentsPerPost + c + 1),
57
+ "posts.comments.text": `Comment${c + 1}`,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return records;
63
+ }
64
+
65
+ describe("simple record processing", () => {
66
+ it("10,000개 레코드 processing - 500ms 이내", async () => {
67
+ const raw = generateSimpleRecords(10_000);
68
+ const meta: ResultMeta = {
69
+ columns: {
70
+ id: "number",
71
+ name: "string",
72
+ email: "string",
73
+ age: "number",
74
+ active: "boolean",
75
+ },
76
+ joins: {},
77
+ };
78
+
79
+ const start = performance.now();
80
+ const result = await parseQueryResult(raw, meta);
81
+ const elapsed = performance.now() - start;
82
+
83
+ expect(result).toHaveLength(10_000);
84
+ expect(elapsed).toBeLessThan(500);
85
+ console.log(` 단순 10,000개: ${elapsed.toFixed(2)}ms`);
86
+ });
87
+
88
+ it("50,000개 레코드 processing - 3000ms 이내", async () => {
89
+ const raw = generateSimpleRecords(50_000);
90
+ const meta: ResultMeta = {
91
+ columns: {
92
+ id: "number",
93
+ name: "string",
94
+ email: "string",
95
+ age: "number",
96
+ active: "boolean",
97
+ },
98
+ joins: {},
99
+ };
100
+
101
+ const start = performance.now();
102
+ const result = await parseQueryResult(raw, meta);
103
+ const elapsed = performance.now() - start;
104
+
105
+ expect(result).toHaveLength(50_000);
106
+ expect(elapsed).toBeLessThan(3000);
107
+ console.log(` 단순 50,000개: ${elapsed.toFixed(2)}ms`);
108
+ });
109
+ });
110
+
111
+ describe("1-level JOIN processing", () => {
112
+ it("1,000 users × 10 posts = 10,000개 레코드 - 600ms 이내", async () => {
113
+ const raw = generateJoinRecords(1_000, 10);
114
+ const meta: ResultMeta = {
115
+ columns: {
116
+ "id": "number",
117
+ "name": "string",
118
+ "posts.id": "number",
119
+ "posts.title": "string",
120
+ "posts.content": "string",
121
+ },
122
+ joins: { posts: { isSingle: false } },
123
+ };
124
+
125
+ const start = performance.now();
126
+ const result = await parseQueryResult(raw, meta);
127
+ const elapsed = performance.now() - start;
128
+
129
+ expect(result).toHaveLength(1_000);
130
+ expect(elapsed).toBeLessThan(600);
131
+ console.log(` JOIN 10,000개 (1000×10): ${elapsed.toFixed(2)}ms`);
132
+ });
133
+
134
+ it("5,000 users × 10 posts = 50,000개 레코드 - 3000ms 이내", async () => {
135
+ const raw = generateJoinRecords(5_000, 10);
136
+ const meta: ResultMeta = {
137
+ columns: {
138
+ "id": "number",
139
+ "name": "string",
140
+ "posts.id": "number",
141
+ "posts.title": "string",
142
+ "posts.content": "string",
143
+ },
144
+ joins: { posts: { isSingle: false } },
145
+ };
146
+
147
+ const start = performance.now();
148
+ const result = await parseQueryResult(raw, meta);
149
+ const elapsed = performance.now() - start;
150
+
151
+ expect(result).toHaveLength(5_000);
152
+ expect(elapsed).toBeLessThan(3000);
153
+ console.log(` JOIN 50,000개 (5000×10): ${elapsed.toFixed(2)}ms`);
154
+ });
155
+ });
156
+
157
+ describe("2-level nested JOIN processing", () => {
158
+ it("100 users × 10 posts × 5 comments = 5,000개 레코드 - 500ms 이내", async () => {
159
+ const raw = generateNestedJoinRecords(100, 10, 5);
160
+ const meta: ResultMeta = {
161
+ columns: {
162
+ "id": "number",
163
+ "name": "string",
164
+ "posts.id": "number",
165
+ "posts.title": "string",
166
+ "posts.comments.id": "number",
167
+ "posts.comments.text": "string",
168
+ },
169
+ joins: {
170
+ "posts": { isSingle: false },
171
+ "posts.comments": { isSingle: false },
172
+ },
173
+ };
174
+
175
+ const start = performance.now();
176
+ const result = await parseQueryResult(raw, meta);
177
+ const elapsed = performance.now() - start;
178
+
179
+ expect(result).toHaveLength(100);
180
+ expect(elapsed).toBeLessThan(500);
181
+ console.log(` 중첩 JOIN 5,000개 (100×10×5): ${elapsed.toFixed(2)}ms`);
182
+ });
183
+
184
+ it("500 users × 10 posts × 5 comments = 25,000개 레코드 - 2000ms 이내", async () => {
185
+ const raw = generateNestedJoinRecords(500, 10, 5);
186
+ const meta: ResultMeta = {
187
+ columns: {
188
+ "id": "number",
189
+ "name": "string",
190
+ "posts.id": "number",
191
+ "posts.title": "string",
192
+ "posts.comments.id": "number",
193
+ "posts.comments.text": "string",
194
+ },
195
+ joins: {
196
+ "posts": { isSingle: false },
197
+ "posts.comments": { isSingle: false },
198
+ },
199
+ };
200
+
201
+ const start = performance.now();
202
+ const result = await parseQueryResult(raw, meta);
203
+ const elapsed = performance.now() - start;
204
+
205
+ expect(result).toHaveLength(500);
206
+ expect(elapsed).toBeLessThan(2000);
207
+ console.log(` 중첩 JOIN 25,000개 (500×10×5): ${elapsed.toFixed(2)}ms`);
208
+ });
209
+ });
210
+ });