@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.
- 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 +104 -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(), // 자기 참조 (상위 매니저)
|
|
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
|
+
// 활성 사용자만 보여주는 뷰
|
|
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
|
+
});
|