@simplysm/orm-common 13.0.69 → 13.0.71

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 +105 -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(), // self-referencing (parent manager)
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
+ // view showing only active users
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
+ // view for user summary info (includes 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
+ * Performance benchmark tests
7
+ *
8
+ * Purpose: measure optimization effect of removing objClone
9
+ * Metrics:
10
+ * - processing time for large simple records
11
+ * - processing time for large JOIN records
12
+ * - processing time for nested JOIN records
13
+ */
14
+ describe("result-parser performance", () => {
15
+ // test data generation helpers
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 records processing - within 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(` simple 10,000: ${elapsed.toFixed(2)}ms`);
86
+ });
87
+
88
+ it("50,000 records processing - within 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(` simple 50,000: ${elapsed.toFixed(2)}ms`);
108
+ });
109
+ });
110
+
111
+ describe("1-level JOIN processing", () => {
112
+ it("1,000 users × 10 posts = 10,000 records - within 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 records - within 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 records - within 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(` nested JOIN 5,000 (100×10×5): ${elapsed.toFixed(2)}ms`);
182
+ });
183
+
184
+ it("500 users × 10 posts × 5 comments = 25,000 records - within 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(` nested JOIN 25,000 (500×10×5): ${elapsed.toFixed(2)}ms`);
208
+ });
209
+ });
210
+ });