@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.
- package/README.md +54 -1447
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +9 -9
- package/dist/create-db-context.js.map +1 -1
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +2 -2
- package/dist/ddl/initialize.js.map +1 -1
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/table-ddl.d.ts +24 -24
- package/dist/ddl/table-ddl.js +4 -4
- package/dist/ddl/table-ddl.js.map +1 -1
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +3 -3
- package/dist/exec/executable.js.map +1 -1
- package/dist/exec/queryable.d.ts +160 -160
- package/dist/exec/queryable.js +119 -119
- package/dist/exec/queryable.js.map +1 -1
- package/dist/exec/search-parser.d.ts +37 -37
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr.d.ts +257 -257
- package/dist/expr/expr.js +265 -265
- package/dist/expr/expr.js.map +1 -1
- package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
- package/dist/query-builder/base/expr-renderer-base.js +2 -2
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
- package/dist/query-builder/base/query-builder-base.d.ts +26 -26
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +22 -22
- package/dist/query-builder/base/query-builder-base.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/schema/factory/column-builder.d.ts +79 -79
- package/dist/schema/factory/column-builder.js +42 -42
- package/dist/schema/factory/index-builder.d.ts +39 -39
- package/dist/schema/factory/index-builder.js +26 -26
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +38 -38
- package/dist/schema/procedure-builder.d.ts +49 -49
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +33 -33
- package/dist/schema/table-builder.d.ts +59 -59
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +43 -43
- package/dist/schema/view-builder.d.ts +49 -49
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +32 -32
- package/dist/types/column.d.ts +22 -22
- package/dist/types/column.js +1 -1
- package/dist/types/column.js.map +1 -1
- package/dist/types/db.d.ts +40 -40
- package/dist/types/expr.d.ts +59 -59
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/query-def.d.ts +44 -44
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +3 -3
- package/dist/utils/result-parser.js.map +1 -1
- package/package.json +5 -5
- package/src/create-db-context.ts +20 -20
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +259 -259
- package/src/ddl/relation-ddl.ts +89 -89
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +189 -189
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +2033 -2033
- package/src/exec/search-parser.ts +57 -57
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +2140 -2140
- package/src/query-builder/base/expr-renderer-base.ts +237 -237
- package/src/query-builder/base/query-builder-base.ts +213 -213
- package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
- package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
- package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
- package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
- package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
- package/src/query-builder/query-builder.ts +19 -19
- package/src/schema/factory/column-builder.ts +423 -423
- package/src/schema/factory/index-builder.ts +164 -164
- package/src/schema/factory/relation-builder.ts +453 -453
- package/src/schema/procedure-builder.ts +232 -232
- package/src/schema/table-builder.ts +319 -319
- package/src/schema/view-builder.ts +221 -221
- package/src/types/column.ts +188 -188
- package/src/types/db.ts +208 -208
- package/src/types/expr.ts +697 -697
- package/src/types/query-def.ts +513 -513
- package/src/utils/result-parser.ts +458 -458
- package/tests/db-context/create-db-context.spec.ts +224 -0
- package/tests/db-context/define-db-context.spec.ts +68 -0
- package/tests/ddl/basic.expected.ts +341 -0
- package/tests/ddl/basic.spec.ts +714 -0
- package/tests/ddl/column-builder.expected.ts +310 -0
- package/tests/ddl/column-builder.spec.ts +637 -0
- package/tests/ddl/index-builder.expected.ts +38 -0
- package/tests/ddl/index-builder.spec.ts +202 -0
- package/tests/ddl/procedure-builder.expected.ts +52 -0
- package/tests/ddl/procedure-builder.spec.ts +234 -0
- package/tests/ddl/relation-builder.expected.ts +36 -0
- package/tests/ddl/relation-builder.spec.ts +372 -0
- package/tests/ddl/table-builder.expected.ts +113 -0
- package/tests/ddl/table-builder.spec.ts +433 -0
- package/tests/ddl/view-builder.expected.ts +38 -0
- package/tests/ddl/view-builder.spec.ts +176 -0
- package/tests/dml/delete.expected.ts +96 -0
- package/tests/dml/delete.spec.ts +160 -0
- package/tests/dml/insert.expected.ts +192 -0
- package/tests/dml/insert.spec.ts +288 -0
- package/tests/dml/update.expected.ts +176 -0
- package/tests/dml/update.spec.ts +318 -0
- package/tests/dml/upsert.expected.ts +215 -0
- package/tests/dml/upsert.spec.ts +242 -0
- package/tests/errors/queryable-errors.spec.ts +177 -0
- package/tests/escape.spec.ts +100 -0
- package/tests/examples/pivot.expected.ts +211 -0
- package/tests/examples/pivot.spec.ts +533 -0
- package/tests/examples/sampling.expected.ts +69 -0
- package/tests/examples/sampling.spec.ts +105 -0
- package/tests/examples/unpivot.expected.ts +120 -0
- package/tests/examples/unpivot.spec.ts +226 -0
- package/tests/exec/search-parser.spec.ts +283 -0
- package/tests/executable/basic.expected.ts +18 -0
- package/tests/executable/basic.spec.ts +54 -0
- package/tests/expr/comparison.expected.ts +282 -0
- package/tests/expr/comparison.spec.ts +400 -0
- package/tests/expr/conditional.expected.ts +134 -0
- package/tests/expr/conditional.spec.ts +276 -0
- package/tests/expr/date.expected.ts +332 -0
- package/tests/expr/date.spec.ts +526 -0
- package/tests/expr/math.expected.ts +62 -0
- package/tests/expr/math.spec.ts +106 -0
- package/tests/expr/string.expected.ts +218 -0
- package/tests/expr/string.spec.ts +356 -0
- package/tests/expr/utility.expected.ts +147 -0
- package/tests/expr/utility.spec.ts +182 -0
- package/tests/select/basic.expected.ts +322 -0
- package/tests/select/basic.spec.ts +502 -0
- package/tests/select/filter.expected.ts +357 -0
- package/tests/select/filter.spec.ts +1068 -0
- package/tests/select/group.expected.ts +169 -0
- package/tests/select/group.spec.ts +244 -0
- package/tests/select/join.expected.ts +582 -0
- package/tests/select/join.spec.ts +805 -0
- package/tests/select/order.expected.ts +150 -0
- package/tests/select/order.spec.ts +189 -0
- package/tests/select/recursive-cte.expected.ts +244 -0
- package/tests/select/recursive-cte.spec.ts +514 -0
- package/tests/select/result-meta.spec.ts +270 -0
- package/tests/select/subquery.expected.ts +363 -0
- package/tests/select/subquery.spec.ts +537 -0
- package/tests/select/view.expected.ts +155 -0
- package/tests/select/view.spec.ts +235 -0
- package/tests/select/window.expected.ts +345 -0
- package/tests/select/window.spec.ts +618 -0
- package/tests/setup/MockExecutor.ts +18 -0
- package/tests/setup/TestDbContext.ts +59 -0
- package/tests/setup/models/Company.ts +13 -0
- package/tests/setup/models/Employee.ts +10 -0
- package/tests/setup/models/MonthlySales.ts +11 -0
- package/tests/setup/models/Post.ts +16 -0
- package/tests/setup/models/Sales.ts +10 -0
- package/tests/setup/models/User.ts +19 -0
- package/tests/setup/procedure/GetAllUsers.ts +9 -0
- package/tests/setup/procedure/GetUserById.ts +12 -0
- package/tests/setup/test-utils.ts +72 -0
- package/tests/setup/views/ActiveUsers.ts +8 -0
- package/tests/setup/views/UserSummary.ts +11 -0
- package/tests/types/nullable-queryable-record.spec.ts +145 -0
- package/tests/utils/result-parser-perf.spec.ts +210 -0
- package/tests/utils/result-parser.spec.ts +701 -0
- package/docs/expressions.md +0 -172
- package/docs/queries.md +0 -444
- 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,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
|
+
});
|