@simplysm/orm-common 13.0.99 → 14.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +312 -276
- package/dist/create-db-context.js.map +1 -6
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/column-ddl.js +41 -35
- package/dist/ddl/column-ddl.js.map +1 -6
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +200 -142
- package/dist/ddl/initialize.js.map +1 -6
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/relation-ddl.js +55 -48
- package/dist/ddl/relation-ddl.js.map +1 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/schema-ddl.js +21 -15
- package/dist/ddl/schema-ddl.js.map +1 -6
- package/dist/ddl/table-ddl.d.ts +20 -20
- package/dist/ddl/table-ddl.js +139 -93
- package/dist/ddl/table-ddl.js.map +1 -6
- package/dist/define-db-context.js +10 -13
- package/dist/define-db-context.js.map +1 -6
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/errors/db-transaction-error.js +53 -19
- package/dist/errors/db-transaction-error.js.map +1 -6
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +94 -40
- package/dist/exec/executable.js.map +1 -6
- package/dist/exec/queryable.d.ts +97 -97
- package/dist/exec/queryable.js +1310 -1204
- package/dist/exec/queryable.js.map +1 -6
- package/dist/exec/search-parser.d.ts +31 -31
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/exec/search-parser.js +158 -59
- package/dist/exec/search-parser.js.map +1 -6
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr-unit.js +24 -18
- package/dist/expr/expr-unit.js.map +1 -6
- package/dist/expr/expr.d.ts +6 -6
- package/dist/expr/expr.js +1872 -1844
- package/dist/expr/expr.js.map +1 -6
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -6
- package/dist/models/system-migration.js +7 -7
- package/dist/models/system-migration.js.map +1 -6
- package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
- package/dist/query-builder/base/expr-renderer-base.js +27 -21
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
- package/dist/query-builder/base/query-builder-base.d.ts +21 -21
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +90 -80
- package/dist/query-builder/base/query-builder-base.js.map +1 -6
- 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 +447 -420
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
- 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 +451 -419
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
- 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 +449 -422
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/query-builder/query-builder.js +13 -13
- package/dist/query-builder/query-builder.js.map +1 -6
- package/dist/schema/factory/column-builder.d.ts +84 -84
- package/dist/schema/factory/column-builder.js +248 -185
- package/dist/schema/factory/column-builder.js.map +1 -6
- package/dist/schema/factory/index-builder.d.ts +38 -38
- package/dist/schema/factory/index-builder.js +144 -85
- package/dist/schema/factory/index-builder.js.map +1 -6
- package/dist/schema/factory/relation-builder.d.ts +91 -91
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +274 -136
- package/dist/schema/factory/relation-builder.js.map +1 -6
- package/dist/schema/procedure-builder.d.ts +51 -51
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +205 -131
- package/dist/schema/procedure-builder.js.map +1 -6
- package/dist/schema/table-builder.d.ts +55 -55
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +274 -205
- package/dist/schema/table-builder.js.map +1 -6
- package/dist/schema/view-builder.d.ts +44 -44
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +189 -116
- package/dist/schema/view-builder.js.map +1 -6
- package/dist/types/column.js +60 -30
- package/dist/types/column.js.map +1 -6
- package/dist/types/db-context-def.d.ts +9 -9
- package/dist/types/db-context-def.js +2 -1
- package/dist/types/db-context-def.js.map +1 -6
- package/dist/types/db.d.ts +47 -47
- package/dist/types/db.js +15 -5
- package/dist/types/db.js.map +1 -6
- package/dist/types/expr.d.ts +81 -81
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/expr.js +3 -1
- package/dist/types/expr.js.map +1 -6
- package/dist/types/query-def.d.ts +46 -46
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/types/query-def.js +31 -24
- package/dist/types/query-def.js.map +1 -6
- package/dist/utils/result-parser.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/package.json +5 -7
- package/src/create-db-context.ts +31 -31
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +38 -38
- package/src/ddl/relation-ddl.ts +6 -6
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +24 -24
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +134 -134
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +13 -13
- package/src/index.ts +8 -8
- package/src/models/system-migration.ts +1 -1
- package/src/query-builder/base/expr-renderer-base.ts +21 -21
- package/src/query-builder/base/query-builder-base.ts +33 -33
- package/src/query-builder/mssql/mssql-expr-renderer.ts +11 -11
- package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
- package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
- package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
- package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +86 -86
- package/src/schema/factory/index-builder.ts +38 -38
- package/src/schema/factory/relation-builder.ts +93 -93
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +45 -45
- package/src/types/column.ts +1 -1
- package/src/types/db-context-def.ts +15 -15
- package/src/types/db.ts +50 -50
- package/src/types/expr.ts +103 -103
- package/src/types/query-def.ts +50 -50
- package/src/utils/result-parser.ts +39 -39
- package/README.md +0 -192
- package/docs/core.md +0 -234
- package/docs/expression.md +0 -234
- package/docs/query-builder.md +0 -93
- package/docs/queryable.md +0 -198
- package/docs/schema-builders.md +0 -463
- package/docs/types.md +0 -445
- package/docs/utilities.md +0 -27
- package/tests/db-context/create-db-context.spec.ts +0 -193
- package/tests/db-context/define-db-context.spec.ts +0 -17
- package/tests/ddl/basic.expected.ts +0 -341
- package/tests/ddl/basic.spec.ts +0 -557
- package/tests/ddl/column-builder.expected.ts +0 -310
- package/tests/ddl/column-builder.spec.ts +0 -525
- package/tests/ddl/index-builder.expected.ts +0 -38
- package/tests/ddl/index-builder.spec.ts +0 -148
- package/tests/ddl/procedure-builder.expected.ts +0 -52
- package/tests/ddl/procedure-builder.spec.ts +0 -128
- package/tests/ddl/relation-builder.expected.ts +0 -36
- package/tests/ddl/relation-builder.spec.ts +0 -171
- package/tests/ddl/table-builder.expected.ts +0 -113
- package/tests/ddl/table-builder.spec.ts +0 -399
- package/tests/ddl/view-builder.expected.ts +0 -38
- package/tests/ddl/view-builder.spec.ts +0 -116
- package/tests/dml/delete.expected.ts +0 -96
- package/tests/dml/delete.spec.ts +0 -127
- package/tests/dml/insert.expected.ts +0 -192
- package/tests/dml/insert.spec.ts +0 -210
- package/tests/dml/update.expected.ts +0 -176
- package/tests/dml/update.spec.ts +0 -222
- package/tests/dml/upsert.expected.ts +0 -215
- package/tests/dml/upsert.spec.ts +0 -190
- package/tests/errors/queryable-errors.spec.ts +0 -126
- package/tests/escape.spec.ts +0 -59
- package/tests/examples/pivot.expected.ts +0 -211
- package/tests/examples/pivot.spec.ts +0 -200
- package/tests/examples/sampling.expected.ts +0 -69
- package/tests/examples/sampling.spec.ts +0 -42
- package/tests/examples/unpivot.expected.ts +0 -120
- package/tests/examples/unpivot.spec.ts +0 -161
- package/tests/exec/search-parser.spec.ts +0 -267
- package/tests/executable/basic.expected.ts +0 -18
- package/tests/executable/basic.spec.ts +0 -54
- package/tests/expr/comparison.expected.ts +0 -282
- package/tests/expr/comparison.spec.ts +0 -334
- package/tests/expr/conditional.expected.ts +0 -134
- package/tests/expr/conditional.spec.ts +0 -249
- package/tests/expr/date.expected.ts +0 -332
- package/tests/expr/date.spec.ts +0 -459
- package/tests/expr/math.expected.ts +0 -62
- package/tests/expr/math.spec.ts +0 -59
- package/tests/expr/string.expected.ts +0 -218
- package/tests/expr/string.spec.ts +0 -300
- package/tests/expr/utility.expected.ts +0 -147
- package/tests/expr/utility.spec.ts +0 -155
- package/tests/select/basic.expected.ts +0 -322
- package/tests/select/basic.spec.ts +0 -433
- package/tests/select/filter.expected.ts +0 -357
- package/tests/select/filter.spec.ts +0 -954
- package/tests/select/group.expected.ts +0 -169
- package/tests/select/group.spec.ts +0 -159
- package/tests/select/join.expected.ts +0 -582
- package/tests/select/join.spec.ts +0 -692
- package/tests/select/order.expected.ts +0 -150
- package/tests/select/order.spec.ts +0 -140
- package/tests/select/recursive-cte.expected.ts +0 -244
- package/tests/select/recursive-cte.spec.ts +0 -514
- package/tests/select/result-meta.spec.ts +0 -270
- package/tests/select/subquery.expected.ts +0 -363
- package/tests/select/subquery.spec.ts +0 -441
- package/tests/select/view.expected.ts +0 -155
- package/tests/select/view.spec.ts +0 -235
- package/tests/select/window.expected.ts +0 -345
- package/tests/select/window.spec.ts +0 -433
- package/tests/setup/MockExecutor.ts +0 -18
- package/tests/setup/TestDbContext.ts +0 -59
- package/tests/setup/models/Company.ts +0 -13
- package/tests/setup/models/Employee.ts +0 -10
- package/tests/setup/models/MonthlySales.ts +0 -11
- package/tests/setup/models/Post.ts +0 -16
- package/tests/setup/models/Sales.ts +0 -10
- package/tests/setup/models/User.ts +0 -19
- package/tests/setup/procedure/GetAllUsers.ts +0 -9
- package/tests/setup/procedure/GetUserById.ts +0 -12
- package/tests/setup/test-utils.ts +0 -72
- package/tests/setup/views/ActiveUsers.ts +0 -8
- package/tests/setup/views/UserSummary.ts +0 -11
- package/tests/types/nullable-queryable-record.spec.ts +0 -97
- package/tests/utils/result-parser-perf.spec.ts +0 -143
- package/tests/utils/result-parser.spec.ts +0 -667
package/dist/exec/queryable.js
CHANGED
|
@@ -1,1270 +1,1376 @@
|
|
|
1
1
|
import { TableBuilder } from "../schema/table-builder.js";
|
|
2
2
|
import { ViewBuilder } from "../schema/view-builder.js";
|
|
3
|
-
import {
|
|
4
|
-
} from "../schema/factory/column-builder.js";
|
|
3
|
+
import {} from "../schema/factory/column-builder.js";
|
|
5
4
|
import { ExprUnit } from "../expr/expr-unit.js";
|
|
6
5
|
import { ArgumentError, obj } from "@simplysm/core-common";
|
|
7
|
-
import {
|
|
8
|
-
ForeignKeyBuilder,
|
|
9
|
-
ForeignKeyTargetBuilder,
|
|
10
|
-
RelationKeyBuilder,
|
|
11
|
-
RelationKeyTargetBuilder
|
|
12
|
-
} from "../schema/factory/relation-builder.js";
|
|
6
|
+
import { ForeignKeyBuilder, ForeignKeyTargetBuilder, RelationKeyBuilder, RelationKeyTargetBuilder, } from "../schema/factory/relation-builder.js";
|
|
13
7
|
import { parseSearchQuery } from "./search-parser.js";
|
|
14
8
|
import { expr } from "../expr/expr.js";
|
|
9
|
+
/**
|
|
10
|
+
* JOIN query builder
|
|
11
|
+
*
|
|
12
|
+
* join/joinSingle 메서드 내부에서 조인할 table을 지정하는 데 사용
|
|
13
|
+
*/
|
|
15
14
|
class JoinQueryable {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
15
|
+
_db;
|
|
16
|
+
_joinAlias;
|
|
17
|
+
constructor(_db, _joinAlias) {
|
|
18
|
+
this._db = _db;
|
|
19
|
+
this._joinAlias = _joinAlias;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 조인할 table 지정
|
|
23
|
+
*
|
|
24
|
+
* @param table - 조인할 table
|
|
25
|
+
* @returns 조인된 Queryable
|
|
26
|
+
*/
|
|
27
|
+
from(table) {
|
|
28
|
+
return queryable(this._db, table, this._joinAlias)();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 조인 결과의 column을 직접 지정
|
|
32
|
+
*
|
|
33
|
+
* @param columns - 커스텀 column 정의
|
|
34
|
+
* @returns 커스텀 column이 적용된 Queryable
|
|
35
|
+
*/
|
|
36
|
+
select(columns) {
|
|
37
|
+
return new Queryable({
|
|
38
|
+
db: this._db,
|
|
39
|
+
as: this._joinAlias,
|
|
40
|
+
columns,
|
|
41
|
+
isCustomColumns: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 여러 Queryable을 UNION으로 결합
|
|
46
|
+
*
|
|
47
|
+
* @param queries - UNION할 Queryable 배열 (최소 2개)
|
|
48
|
+
* @returns UNION된 Queryable
|
|
49
|
+
* @throws 2개 미만의 queryable이 전달되면 에러
|
|
50
|
+
*/
|
|
51
|
+
union(...queries) {
|
|
52
|
+
if (queries.length < 2) {
|
|
53
|
+
throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
|
|
54
|
+
provided: queries.length,
|
|
55
|
+
minimum: 2,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const first = queries[0];
|
|
59
|
+
return new Queryable({
|
|
60
|
+
db: first.meta.db,
|
|
61
|
+
from: queries, // Queryable[] 배열로 저장
|
|
62
|
+
as: this._joinAlias,
|
|
63
|
+
columns: transformColumnsAlias(first.meta.columns, this._joinAlias, ""),
|
|
64
|
+
});
|
|
56
65
|
}
|
|
57
|
-
const first = queries[0];
|
|
58
|
-
return new Queryable({
|
|
59
|
-
db: first.meta.db,
|
|
60
|
-
from: queries,
|
|
61
|
-
// stored as Queryable[] array
|
|
62
|
-
as: this._joinAlias,
|
|
63
|
-
columns: transformColumnsAlias(first.meta.columns, this._joinAlias, "")
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 재귀 CTE (Common Table Expression) builder
|
|
69
|
+
*
|
|
70
|
+
* recursive() 메서드 내부에서 사용되며, 재귀 쿼리의 본문을 정의한다
|
|
71
|
+
*
|
|
72
|
+
* @template TBaseData - Base query data type
|
|
73
|
+
*/
|
|
67
74
|
class RecursiveQueryable {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* specify the target table for recursive query
|
|
74
|
-
*
|
|
75
|
-
* @param table - Target table to recurse
|
|
76
|
-
* @returns Queryable with self property added (for self-reference)
|
|
77
|
-
*/
|
|
78
|
-
from(table) {
|
|
79
|
-
const selfAlias = `${this._cteName}.self`;
|
|
80
|
-
return queryable(this._baseQr.meta.db, table, this._cteName)().join(
|
|
81
|
-
"self",
|
|
82
|
-
() => new Queryable({
|
|
83
|
-
db: this._baseQr.meta.db,
|
|
84
|
-
from: this._cteName,
|
|
85
|
-
as: selfAlias,
|
|
86
|
-
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
87
|
-
isCustomColumns: false
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Directly specify columns in recursive query
|
|
93
|
-
*
|
|
94
|
-
* @param columns - Custom column definition
|
|
95
|
-
* @returns Queryable with self property added
|
|
96
|
-
*/
|
|
97
|
-
select(columns) {
|
|
98
|
-
const selfAlias = `${this._cteName}.self`;
|
|
99
|
-
return new Queryable({
|
|
100
|
-
db: this._baseQr.meta.db,
|
|
101
|
-
as: this._cteName,
|
|
102
|
-
columns,
|
|
103
|
-
isCustomColumns: true
|
|
104
|
-
}).join(
|
|
105
|
-
"self",
|
|
106
|
-
() => new Queryable({
|
|
107
|
-
db: this._baseQr.meta.db,
|
|
108
|
-
from: this._cteName,
|
|
109
|
-
as: selfAlias,
|
|
110
|
-
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
111
|
-
isCustomColumns: false
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Combine multiple Queryables with UNION (for recursive query)
|
|
117
|
-
*
|
|
118
|
-
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
119
|
-
* @returns UNION Queryable with self property added
|
|
120
|
-
* @throws If less than 2 queryables are passed
|
|
121
|
-
*/
|
|
122
|
-
union(...queries) {
|
|
123
|
-
if (queries.length < 2) {
|
|
124
|
-
throw new ArgumentError("union requires at least 2 queryables.", {
|
|
125
|
-
provided: queries.length,
|
|
126
|
-
minimum: 2
|
|
127
|
-
});
|
|
75
|
+
_baseQr;
|
|
76
|
+
_cteName;
|
|
77
|
+
constructor(_baseQr, _cteName) {
|
|
78
|
+
this._baseQr = _baseQr;
|
|
79
|
+
this._cteName = _cteName;
|
|
128
80
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
isCustomColumns: false
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
class Queryable {
|
|
150
|
-
constructor(meta) {
|
|
151
|
-
this.meta = meta;
|
|
152
|
-
}
|
|
153
|
-
//#region ========== option - SELECT / DISTINCT / LOCK ==========
|
|
154
|
-
/**
|
|
155
|
-
* Specify columns to SELECT.
|
|
156
|
-
*
|
|
157
|
-
* @param fn - Column mapping function. Receives original columns and returns new column structure
|
|
158
|
-
* @returns Queryable with new column structure applied
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```typescript
|
|
162
|
-
* db.user().select((u) => ({
|
|
163
|
-
* userName: u.name,
|
|
164
|
-
* userEmail: u.email,
|
|
165
|
-
* }))
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
|
-
select(fn) {
|
|
169
|
-
if (Array.isArray(this.meta.from)) {
|
|
170
|
-
const newFroms = this.meta.from.map((from) => from.select(fn));
|
|
171
|
-
return new Queryable({
|
|
172
|
-
...this.meta,
|
|
173
|
-
from: newFroms,
|
|
174
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
|
|
175
|
-
});
|
|
81
|
+
/**
|
|
82
|
+
* 재귀 query의 대상 table 지정
|
|
83
|
+
*
|
|
84
|
+
* @param table - 재귀할 대상 table
|
|
85
|
+
* @returns self 속성이 추가된 Queryable (자기 참조용)
|
|
86
|
+
*/
|
|
87
|
+
from(table) {
|
|
88
|
+
const selfAlias = `${this._cteName}.self`;
|
|
89
|
+
return queryable(this._baseQr.meta.db, table, this._cteName)().join("self", () => new Queryable({
|
|
90
|
+
db: this._baseQr.meta.db,
|
|
91
|
+
from: this._cteName,
|
|
92
|
+
as: selfAlias,
|
|
93
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
94
|
+
isCustomColumns: false,
|
|
95
|
+
}));
|
|
176
96
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (Array.isArray(this.meta.from)) {
|
|
198
|
-
const newFroms = this.meta.from.map((from) => from.distinct());
|
|
199
|
-
return new Queryable({
|
|
200
|
-
...this.meta,
|
|
201
|
-
from: newFroms
|
|
202
|
-
});
|
|
97
|
+
/**
|
|
98
|
+
* 재귀 query의 column을 직접 지정
|
|
99
|
+
*
|
|
100
|
+
* @param columns - 커스텀 column 정의
|
|
101
|
+
* @returns self 속성이 추가된 Queryable
|
|
102
|
+
*/
|
|
103
|
+
select(columns) {
|
|
104
|
+
const selfAlias = `${this._cteName}.self`;
|
|
105
|
+
return new Queryable({
|
|
106
|
+
db: this._baseQr.meta.db,
|
|
107
|
+
as: this._cteName,
|
|
108
|
+
columns,
|
|
109
|
+
isCustomColumns: true,
|
|
110
|
+
}).join("self", () => new Queryable({
|
|
111
|
+
db: this._baseQr.meta.db,
|
|
112
|
+
from: this._cteName,
|
|
113
|
+
as: selfAlias,
|
|
114
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
115
|
+
isCustomColumns: false,
|
|
116
|
+
}));
|
|
203
117
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
118
|
+
/**
|
|
119
|
+
* 여러 Queryable을 UNION으로 결합 (재귀 query용)
|
|
120
|
+
*
|
|
121
|
+
* @param queries - UNION할 Queryable 배열 (최소 2개)
|
|
122
|
+
* @returns self 속성이 추가된 UNION Queryable
|
|
123
|
+
* @throws 2개 미만의 queryable이 전달되면 에러
|
|
124
|
+
*/
|
|
125
|
+
union(...queries) {
|
|
126
|
+
if (queries.length < 2) {
|
|
127
|
+
throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
|
|
128
|
+
provided: queries.length,
|
|
129
|
+
minimum: 2,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const first = queries[0];
|
|
133
|
+
const selfAlias = `${this._cteName}.self`;
|
|
134
|
+
return new Queryable({
|
|
135
|
+
db: first.meta.db,
|
|
136
|
+
from: queries, // Queryable[] 배열로 저장
|
|
137
|
+
as: this._cteName,
|
|
138
|
+
columns: transformColumnsAlias(first.meta.columns, this._cteName, ""),
|
|
139
|
+
}).join("self", () => new Queryable({
|
|
140
|
+
db: this._baseQr.meta.db,
|
|
141
|
+
from: this._cteName,
|
|
142
|
+
as: selfAlias,
|
|
143
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
144
|
+
isCustomColumns: false,
|
|
145
|
+
}));
|
|
233
146
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Query builder 클래스
|
|
150
|
+
*
|
|
151
|
+
* 체이닝 방식으로 table/view에 대한 SELECT, INSERT, UPDATE, DELETE query를 구성
|
|
152
|
+
*
|
|
153
|
+
* @template TData - Query 결과의 데이터 타입
|
|
154
|
+
* @template TFrom - 소스 table (CUD 연산에 필요)
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* // Basic query
|
|
159
|
+
* const users = await db.user()
|
|
160
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
161
|
+
* .orderBy((u) => u.name)
|
|
162
|
+
* .execute();
|
|
163
|
+
*
|
|
164
|
+
* // JOIN query
|
|
165
|
+
* const posts = await db.post()
|
|
166
|
+
* .include((p) => p.user)
|
|
167
|
+
* .execute();
|
|
168
|
+
*
|
|
169
|
+
* // INSERT
|
|
170
|
+
* await db.user().insert([{ name: "Gildong Hong", email: "test@test.com" }]);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export class Queryable {
|
|
174
|
+
meta;
|
|
175
|
+
constructor(meta) {
|
|
176
|
+
this.meta = meta;
|
|
262
177
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
178
|
+
//#region ========== option - SELECT / DISTINCT / LOCK ==========
|
|
179
|
+
/**
|
|
180
|
+
* SELECT할 column 지정.
|
|
181
|
+
*
|
|
182
|
+
* @param fn - Column 매핑 함수. 원본 column을 받아 새 column 구조를 반환
|
|
183
|
+
* @returns 새 column 구조가 적용된 Queryable
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* db.user().select((u) => ({
|
|
188
|
+
* userName: u.name,
|
|
189
|
+
* userEmail: u.email,
|
|
190
|
+
* }))
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
select(fn) {
|
|
194
|
+
if (Array.isArray(this.meta.from)) {
|
|
195
|
+
const newFroms = this.meta.from.map((from) => from.select(fn));
|
|
196
|
+
return new Queryable({
|
|
197
|
+
...this.meta,
|
|
198
|
+
from: newFroms,
|
|
199
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const newColumns = fn(this.meta.columns);
|
|
203
|
+
return new Queryable({
|
|
204
|
+
...this.meta,
|
|
205
|
+
columns: newColumns,
|
|
206
|
+
isCustomColumns: true,
|
|
207
|
+
});
|
|
291
208
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
209
|
+
/**
|
|
210
|
+
* 중복 행 제거를 위한 DISTINCT 옵션 적용
|
|
211
|
+
*
|
|
212
|
+
* @returns DISTINCT가 적용된 Queryable
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* db.user()
|
|
217
|
+
* .select((u) => ({ name: u.name }))
|
|
218
|
+
* .distinct()
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
distinct() {
|
|
222
|
+
if (Array.isArray(this.meta.from)) {
|
|
223
|
+
const newFroms = this.meta.from.map((from) => from.distinct());
|
|
224
|
+
return new Queryable({
|
|
225
|
+
...this.meta,
|
|
226
|
+
from: newFroms,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return new Queryable({
|
|
230
|
+
...this.meta,
|
|
231
|
+
distinct: true,
|
|
232
|
+
});
|
|
297
233
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
234
|
+
/**
|
|
235
|
+
* 행 잠금 적용 (FOR UPDATE)
|
|
236
|
+
*
|
|
237
|
+
* 트랜잭션 내에서 선택된 행에 대한 배타적 잠금 획득
|
|
238
|
+
*
|
|
239
|
+
* @returns 잠금이 적용된 Queryable
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* await db.connect(async () => {
|
|
244
|
+
* const user = await db.user()
|
|
245
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
246
|
+
* .lock()
|
|
247
|
+
* .single();
|
|
248
|
+
* });
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
lock() {
|
|
252
|
+
if (Array.isArray(this.meta.from)) {
|
|
253
|
+
const newFroms = this.meta.from.map((from) => from.lock());
|
|
254
|
+
return new Queryable({
|
|
255
|
+
...this.meta,
|
|
256
|
+
from: newFroms,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return new Queryable({
|
|
260
|
+
...this.meta,
|
|
261
|
+
lock: true,
|
|
262
|
+
});
|
|
326
263
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region ========== restrict - TOP / LIMIT ==========
|
|
266
|
+
/**
|
|
267
|
+
* 상위 N개 행만 선택 (ORDER BY 없이도 사용 가능)
|
|
268
|
+
*
|
|
269
|
+
* @param count - 선택할 행 수
|
|
270
|
+
* @returns TOP이 적용된 Queryable
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // Latest 10 users
|
|
275
|
+
* db.user()
|
|
276
|
+
* .orderBy((u) => u.createdAt, "DESC")
|
|
277
|
+
* .top(10)
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
top(count) {
|
|
281
|
+
if (Array.isArray(this.meta.from)) {
|
|
282
|
+
const newFroms = this.meta.from.map((from) => from.top(count));
|
|
283
|
+
return new Queryable({
|
|
284
|
+
...this.meta,
|
|
285
|
+
from: newFroms,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return new Queryable({
|
|
289
|
+
...this.meta,
|
|
290
|
+
top: count,
|
|
291
|
+
});
|
|
355
292
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
293
|
+
/**
|
|
294
|
+
* 페이지네이션을 위한 LIMIT/OFFSET 설정.
|
|
295
|
+
* 먼저 orderBy()를 호출해야 함.
|
|
296
|
+
*
|
|
297
|
+
* @param skip - 건너뛸 행 수 (OFFSET)
|
|
298
|
+
* @param take - 가져올 행 수 (LIMIT)
|
|
299
|
+
* @returns 페이지네이션이 적용된 Queryable
|
|
300
|
+
* @throws ORDER BY 절이 없으면 에러
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* db.user
|
|
305
|
+
* .orderBy((u) => u.createdAt)
|
|
306
|
+
* .limit(0, 20) // first 20
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
limit(skip, take) {
|
|
310
|
+
if (Array.isArray(this.meta.from)) {
|
|
311
|
+
const newFroms = this.meta.from.map((from) => from.limit(skip, take));
|
|
312
|
+
return new Queryable({
|
|
313
|
+
...this.meta,
|
|
314
|
+
from: newFroms,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (!this.meta.orderBy) {
|
|
318
|
+
throw new ArgumentError("limit()은 ORDER BY 절이 필요합니다.", {
|
|
319
|
+
method: "limit",
|
|
320
|
+
required: "orderBy",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return new Queryable({
|
|
324
|
+
...this.meta,
|
|
325
|
+
limit: [skip, take],
|
|
326
|
+
});
|
|
387
327
|
}
|
|
388
|
-
|
|
389
|
-
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region ========== sorting - ORDER BY ==========
|
|
330
|
+
/**
|
|
331
|
+
* 정렬 조건 추가. 여러 번 호출 시 순서대로 적용됨.
|
|
332
|
+
*
|
|
333
|
+
* @param fn - 정렬할 column을 반환하는 함수
|
|
334
|
+
* @param orderBy - 정렬 방향 (ASC/DESC). 기본값: ASC
|
|
335
|
+
* @returns 정렬 조건이 추가된 Queryable
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* db.user
|
|
340
|
+
* .orderBy((u) => u.name) // name ASC
|
|
341
|
+
* .orderBy((u) => u.age, "DESC") // age DESC
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
orderBy(fn, orderBy) {
|
|
345
|
+
if (Array.isArray(this.meta.from)) {
|
|
346
|
+
const newFroms = this.meta.from.map((from) => from.orderBy(fn, orderBy));
|
|
347
|
+
return new Queryable({
|
|
348
|
+
...this.meta,
|
|
349
|
+
from: newFroms,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const column = fn(this.meta.columns);
|
|
353
|
+
return new Queryable({
|
|
354
|
+
...this.meta,
|
|
355
|
+
orderBy: [...(this.meta.orderBy ?? []), [column, orderBy]],
|
|
356
|
+
});
|
|
390
357
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region ========== Search - WHERE ==========
|
|
360
|
+
/**
|
|
361
|
+
* WHERE 조건 추가. 여러 번 호출 시 AND로 결합됨.
|
|
362
|
+
*
|
|
363
|
+
* @param predicate - 조건 배열을 반환하는 함수
|
|
364
|
+
* @returns 조건이 추가된 Queryable
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* db.user
|
|
369
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
370
|
+
* .where((u) => [expr.gte(u.age, 18)])
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
where(predicate) {
|
|
374
|
+
if (Array.isArray(this.meta.from)) {
|
|
375
|
+
const newFroms = this.meta.from.map((from) => from.where(predicate));
|
|
376
|
+
return new Queryable({
|
|
377
|
+
...this.meta,
|
|
378
|
+
from: newFroms,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const conditions = predicate(this.meta.columns);
|
|
382
|
+
return new Queryable({
|
|
383
|
+
...this.meta,
|
|
384
|
+
where: [...(this.meta.where ?? []), ...conditions],
|
|
385
|
+
});
|
|
406
386
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
387
|
+
/**
|
|
388
|
+
* 텍스트 검색 수행
|
|
389
|
+
*
|
|
390
|
+
* 검색 구문은 {@link parseSearchQuery} 참조
|
|
391
|
+
* - 공백으로 구분된 단어는 OR 조건
|
|
392
|
+
* - `+`로 시작하는 단어는 필수 포함 (AND 조건)
|
|
393
|
+
* - `-`로 시작하는 단어는 제외 (NOT 조건)
|
|
394
|
+
*
|
|
395
|
+
* @param fn - 검색 대상 column을 반환하는 함수
|
|
396
|
+
* @param searchText - 검색 텍스트
|
|
397
|
+
* @returns 검색 조건이 추가된 Queryable
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* db.user()
|
|
402
|
+
* .search((u) => [u.name, u.email], "John Doe -withdrawn")
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
search(fn, searchText) {
|
|
406
|
+
if (Array.isArray(this.meta.from)) {
|
|
407
|
+
const newFroms = this.meta.from.map((from) => from.search(fn, searchText));
|
|
408
|
+
return new Queryable({
|
|
409
|
+
...this.meta,
|
|
410
|
+
from: newFroms,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (searchText.trim() === "") {
|
|
414
|
+
return this;
|
|
415
|
+
}
|
|
416
|
+
const columns = fn(this.meta.columns);
|
|
417
|
+
const parsed = parseSearchQuery(searchText);
|
|
418
|
+
const conditions = [];
|
|
419
|
+
// OR 조건: 아무 column에서 아무 패턴이 매칭되면 일치
|
|
420
|
+
if (parsed.or.length === 1) {
|
|
421
|
+
const pattern = parsed.or[0];
|
|
422
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
423
|
+
conditions.push(expr.or(columnMatches));
|
|
424
|
+
}
|
|
425
|
+
else if (parsed.or.length > 1) {
|
|
426
|
+
const orConditions = parsed.or.map((pattern) => {
|
|
427
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
428
|
+
return expr.or(columnMatches);
|
|
429
|
+
});
|
|
430
|
+
conditions.push(expr.or(orConditions));
|
|
431
|
+
}
|
|
432
|
+
// MUST 조건: 각 패턴이 최소 하나의 column에서 매칭되어야 함 (AND)
|
|
433
|
+
for (const pattern of parsed.must) {
|
|
434
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
435
|
+
conditions.push(expr.or(columnMatches));
|
|
436
|
+
}
|
|
437
|
+
// NOT 조건: 아무 column에서도 매칭되지 않아야 함 (AND NOT)
|
|
438
|
+
for (const pattern of parsed.not) {
|
|
439
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
440
|
+
conditions.push(expr.not(expr.or(columnMatches)));
|
|
441
|
+
}
|
|
442
|
+
if (conditions.length === 0) {
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
return this.where(() => [expr.and(conditions)]);
|
|
410
446
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region ========== Group - GROUP BY / HAVING ==========
|
|
449
|
+
/**
|
|
450
|
+
* GROUP BY 절 추가
|
|
451
|
+
*
|
|
452
|
+
* @param fn - 그룹화할 column을 반환하는 함수
|
|
453
|
+
* @returns GROUP BY가 적용된 Queryable
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* db.order()
|
|
458
|
+
* .select((o) => ({
|
|
459
|
+
* userId: o.userId,
|
|
460
|
+
* totalAmount: expr.sum(o.amount),
|
|
461
|
+
* }))
|
|
462
|
+
* .groupBy((o) => [o.userId])
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
groupBy(fn) {
|
|
466
|
+
if (Array.isArray(this.meta.from)) {
|
|
467
|
+
const newFroms = this.meta.from.map((from) => from.groupBy(fn));
|
|
468
|
+
return new Queryable({
|
|
469
|
+
...this.meta,
|
|
470
|
+
from: newFroms,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const groupBy = fn(this.meta.columns);
|
|
474
|
+
return new Queryable({ ...this.meta, groupBy });
|
|
414
475
|
}
|
|
415
|
-
|
|
416
|
-
|
|
476
|
+
/**
|
|
477
|
+
* HAVING 절 추가 (GROUP BY 이후 필터링)
|
|
478
|
+
*
|
|
479
|
+
* @param predicate - 조건 배열을 반환하는 함수
|
|
480
|
+
* @returns HAVING이 적용된 Queryable
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```typescript
|
|
484
|
+
* db.order()
|
|
485
|
+
* .select((o) => ({
|
|
486
|
+
* userId: o.userId,
|
|
487
|
+
* totalAmount: expr.sum(o.amount),
|
|
488
|
+
* }))
|
|
489
|
+
* .groupBy((o) => [o.userId])
|
|
490
|
+
* .having((o) => [expr.gte(o.totalAmount, 10000)])
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
having(predicate) {
|
|
494
|
+
if (Array.isArray(this.meta.from)) {
|
|
495
|
+
const newFroms = this.meta.from.map((from) => from.having(predicate));
|
|
496
|
+
return new Queryable({
|
|
497
|
+
...this.meta,
|
|
498
|
+
from: newFroms,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const conditions = predicate(this.meta.columns);
|
|
502
|
+
return new Queryable({
|
|
503
|
+
...this.meta,
|
|
504
|
+
having: [...(this.meta.having ?? []), ...conditions],
|
|
505
|
+
});
|
|
417
506
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region ========== join - JOIN / JOIN SINGLE ==========
|
|
509
|
+
/**
|
|
510
|
+
* 1:N 관계에 대한 LEFT OUTER JOIN 수행 (결과에 배열로 추가)
|
|
511
|
+
*
|
|
512
|
+
* @param as - 결과에 추가할 속성 이름
|
|
513
|
+
* @param fn - 조인 조건을 정의하는 콜백 함수
|
|
514
|
+
* @returns 조인 결과가 배열로 추가된 Queryable
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```typescript
|
|
518
|
+
* db.user()
|
|
519
|
+
* .join("posts", (qr, u) =>
|
|
520
|
+
* qr.from(Post)
|
|
521
|
+
* .where((p) => [expr.eq(p.userId, u.id)])
|
|
522
|
+
* )
|
|
523
|
+
* // Result: { id, name, posts: [{ id, title }, ...] }
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
join(as, fn) {
|
|
527
|
+
if (Array.isArray(this.meta.from)) {
|
|
528
|
+
const newFroms = this.meta.from.map((from) => from.join(as, fn));
|
|
529
|
+
return new Queryable({
|
|
530
|
+
...this.meta,
|
|
531
|
+
from: newFroms,
|
|
532
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
// 1. join alias Generate
|
|
536
|
+
const joinAlias = `${this.meta.as}.${as}`;
|
|
537
|
+
// 2. Transform target → Queryable (pass alias)
|
|
538
|
+
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
539
|
+
// 3. Execute fn (returns Queryable with conditions like where added)
|
|
540
|
+
const resultQr = fn(joinQr, this.meta.columns);
|
|
541
|
+
// 4. Add join result to new columns
|
|
542
|
+
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
543
|
+
return new Queryable({
|
|
544
|
+
...this.meta,
|
|
545
|
+
columns: {
|
|
546
|
+
...this.meta.columns,
|
|
547
|
+
[as]: [joinColumns],
|
|
548
|
+
},
|
|
549
|
+
isCustomColumns: true,
|
|
550
|
+
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: false }],
|
|
551
|
+
});
|
|
445
552
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
553
|
+
/**
|
|
554
|
+
* N:1 또는 1:1 관계에 대한 LEFT OUTER JOIN 수행 (결과에 단일 객체로 추가)
|
|
555
|
+
*
|
|
556
|
+
* @param as - 결과에 추가할 속성 이름
|
|
557
|
+
* @param fn - 조인 조건을 정의하는 콜백 함수
|
|
558
|
+
* @returns 조인 결과가 단일 객체로 추가된 Queryable
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```typescript
|
|
562
|
+
* db.post()
|
|
563
|
+
* .joinSingle("user", (qr, p) =>
|
|
564
|
+
* qr.from(User)
|
|
565
|
+
* .where((u) => [expr.eq(u.id, p.userId)])
|
|
566
|
+
* )
|
|
567
|
+
* // Result: { id, title, user: { id, name } | undefined }
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
joinSingle(as, fn) {
|
|
571
|
+
if (Array.isArray(this.meta.from)) {
|
|
572
|
+
const newFroms = this.meta.from.map((from) => from.joinSingle(as, fn));
|
|
573
|
+
return new Queryable({
|
|
574
|
+
...this.meta,
|
|
575
|
+
from: newFroms,
|
|
576
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// 1. join alias Generate
|
|
580
|
+
const joinAlias = `${this.meta.as}.${as}`;
|
|
581
|
+
// 2. Transform target → Queryable (pass alias)
|
|
582
|
+
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
583
|
+
// 3. Execute fn (returns Queryable with conditions like where added)
|
|
584
|
+
const resultQr = fn(joinQr, this.meta.columns);
|
|
585
|
+
// 4. Add join result to new columns
|
|
586
|
+
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
587
|
+
return new Queryable({
|
|
588
|
+
...this.meta,
|
|
589
|
+
columns: {
|
|
590
|
+
...this.meta.columns,
|
|
591
|
+
[as]: joinColumns,
|
|
592
|
+
},
|
|
593
|
+
isCustomColumns: true,
|
|
594
|
+
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: true }],
|
|
595
|
+
});
|
|
473
596
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region ========== join - INCLUDE ==========
|
|
599
|
+
/**
|
|
600
|
+
* 관련 table을 자동으로 JOIN.
|
|
601
|
+
* TableBuilder에 정의된 FK/FKT 관계를 기반으로 동작.
|
|
602
|
+
*
|
|
603
|
+
* @param fn - 포함할 관계를 선택하는 함수 (PathProxy를 통해 타입 체크)
|
|
604
|
+
* @returns JOIN이 추가된 Queryable
|
|
605
|
+
* @throws 관계가 정의되지 않은 경우 에러
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* // Single relationship include
|
|
610
|
+
* db.post.include((p) => p.user)
|
|
611
|
+
*
|
|
612
|
+
* // Nested relationship include
|
|
613
|
+
* db.post.include((p) => p.user.company)
|
|
614
|
+
*
|
|
615
|
+
* // Multiple relationship include
|
|
616
|
+
* db.user
|
|
617
|
+
* .include((u) => u.company)
|
|
618
|
+
* .include((u) => u.posts)
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
include(fn) {
|
|
622
|
+
if (Array.isArray(this.meta.from)) {
|
|
623
|
+
const newFroms = this.meta.from.map((from) => from.include(fn));
|
|
624
|
+
return new Queryable({
|
|
625
|
+
...this.meta,
|
|
626
|
+
from: newFroms,
|
|
627
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const proxy = createPathProxy();
|
|
631
|
+
const result = fn(proxy);
|
|
632
|
+
const relationChain = result[PATH_SYMBOL].join(".");
|
|
633
|
+
return this._include(relationChain);
|
|
507
634
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
635
|
+
_include(relationChain) {
|
|
636
|
+
const relationNames = relationChain.split(".");
|
|
637
|
+
let result = this;
|
|
638
|
+
let currentTable = this.meta.from;
|
|
639
|
+
const chainParts = [];
|
|
640
|
+
for (const relationName of relationNames) {
|
|
641
|
+
if (!(currentTable instanceof TableBuilder)) {
|
|
642
|
+
throw new Error("include()는 TableBuilder 기반 queryable에서만 사용할 수 있습니다.");
|
|
643
|
+
}
|
|
644
|
+
const parentChain = chainParts.join(".");
|
|
645
|
+
chainParts.push(relationName);
|
|
646
|
+
// 이미 JOIN된 경우 중복 추가 방지
|
|
647
|
+
const targetAlias = `${result.meta.as}.${chainParts.join(".")}`;
|
|
648
|
+
const existingJoin = result.meta.joins?.find((j) => j.queryable.meta.as === targetAlias);
|
|
649
|
+
if (existingJoin) {
|
|
650
|
+
// 기존 JOIN의 table로 currentTable 갱신 후 계속
|
|
651
|
+
const existingFrom = existingJoin.queryable.meta.from;
|
|
652
|
+
if (existingFrom instanceof TableBuilder) {
|
|
653
|
+
currentTable = existingFrom;
|
|
654
|
+
}
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const relationDef = currentTable.meta.relations?.[relationName];
|
|
658
|
+
if (relationDef == null) {
|
|
659
|
+
throw new Error(`관계 '${relationName}'을(를) 찾을 수 없습니다.`);
|
|
660
|
+
}
|
|
661
|
+
if (relationDef instanceof ForeignKeyBuilder || relationDef instanceof RelationKeyBuilder) {
|
|
662
|
+
// FK/RelationKey (N:1): Post.user → User
|
|
663
|
+
// condition: Post.userId = User.id
|
|
664
|
+
const targetTable = relationDef.meta.targetFn();
|
|
665
|
+
const fkColKeys = relationDef.meta.columns;
|
|
666
|
+
const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
|
|
667
|
+
result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
|
|
668
|
+
const qr = joinQr.from(targetTable);
|
|
669
|
+
// FKT join is stored as array, so use first element if array
|
|
670
|
+
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
671
|
+
const srcCols = (Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw);
|
|
672
|
+
const conditions = [];
|
|
673
|
+
for (let i = 0; i < fkColKeys.length; i++) {
|
|
674
|
+
const fkCol = srcCols[fkColKeys[i]];
|
|
675
|
+
const pkCol = qr.meta.columns[targetPkColKeys[i]];
|
|
676
|
+
conditions.push(expr.eq(pkCol, fkCol));
|
|
677
|
+
}
|
|
678
|
+
return qr.where(() => conditions);
|
|
679
|
+
});
|
|
680
|
+
currentTable = targetTable;
|
|
681
|
+
}
|
|
682
|
+
else if (relationDef instanceof ForeignKeyTargetBuilder ||
|
|
683
|
+
relationDef instanceof RelationKeyTargetBuilder) {
|
|
684
|
+
// FKT/RelationKeyTarget (1:N or 1:1): User.posts → Post[]
|
|
685
|
+
// condition: Post.userId = User.id
|
|
686
|
+
const targetTable = relationDef.meta.targetTableFn();
|
|
687
|
+
const fkRelName = relationDef.meta.relationName;
|
|
688
|
+
const sourceFk = targetTable.meta.relations?.[fkRelName];
|
|
689
|
+
if (!(sourceFk instanceof ForeignKeyBuilder) && !(sourceFk instanceof RelationKeyBuilder)) {
|
|
690
|
+
throw new Error(`'${relationName}'이(가) 참조하는 '${fkRelName}'은(는) ` +
|
|
691
|
+
`${targetTable.meta.name} 테이블에서 유효한 ForeignKey/RelationKey가 아닙니다.`);
|
|
692
|
+
}
|
|
693
|
+
const sourceTable = targetTable;
|
|
694
|
+
const isSingle = relationDef.meta.isSingle ?? false;
|
|
695
|
+
const fkColKeys = sourceFk.meta.columns;
|
|
696
|
+
const pkColKeys = getMatchedPrimaryKeys(fkColKeys, currentTable);
|
|
697
|
+
const buildJoin = (joinQr, parentCols) => {
|
|
698
|
+
const qr = joinQr.from(sourceTable);
|
|
699
|
+
// FKT join is stored as array, so use first element if array
|
|
700
|
+
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
701
|
+
const srcCols = (Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw);
|
|
702
|
+
const conditions = [];
|
|
703
|
+
for (let i = 0; i < fkColKeys.length; i++) {
|
|
704
|
+
const pkCol = srcCols[pkColKeys[i]];
|
|
705
|
+
const fkCol = qr.meta.columns[fkColKeys[i]];
|
|
706
|
+
conditions.push(expr.eq(fkCol, pkCol));
|
|
707
|
+
}
|
|
708
|
+
return qr.where(() => conditions);
|
|
709
|
+
};
|
|
710
|
+
result = isSingle
|
|
711
|
+
? result.joinSingle(chainParts.join("."), buildJoin)
|
|
712
|
+
: result.join(chainParts.join("."), buildJoin);
|
|
713
|
+
currentTable = sourceTable;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
547
717
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
*
|
|
577
|
-
* // Nested relationship include
|
|
578
|
-
* db.post.include((p) => p.user.company)
|
|
579
|
-
*
|
|
580
|
-
* // Multiple relationship include
|
|
581
|
-
* db.user
|
|
582
|
-
* .include((u) => u.company)
|
|
583
|
-
* .include((u) => u.posts)
|
|
584
|
-
* ```
|
|
585
|
-
*/
|
|
586
|
-
include(fn) {
|
|
587
|
-
if (Array.isArray(this.meta.from)) {
|
|
588
|
-
const newFroms = this.meta.from.map((from) => from.include(fn));
|
|
589
|
-
return new Queryable({
|
|
590
|
-
...this.meta,
|
|
591
|
-
from: newFroms,
|
|
592
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
|
|
593
|
-
});
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region ========== Subquery - WRAP / UNION ==========
|
|
720
|
+
/**
|
|
721
|
+
* Wrap the current Queryable as a Subquery
|
|
722
|
+
*
|
|
723
|
+
* Required when using count() after distinct() or groupBy()
|
|
724
|
+
*
|
|
725
|
+
* @returns Queryable wrapped as a Subquery
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```typescript
|
|
729
|
+
* // Count after DISTINCT
|
|
730
|
+
* const count = await db.user()
|
|
731
|
+
* .select((u) => ({ name: u.name }))
|
|
732
|
+
* .distinct()
|
|
733
|
+
* .wrap()
|
|
734
|
+
* .count();
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
wrap() {
|
|
738
|
+
// Wrap the current Queryable as a Subquery
|
|
739
|
+
const wrapAlias = this.meta.db.getNextAlias();
|
|
740
|
+
return new Queryable({
|
|
741
|
+
db: this.meta.db,
|
|
742
|
+
from: this,
|
|
743
|
+
as: wrapAlias,
|
|
744
|
+
columns: transformColumnsAlias(this.meta.columns, wrapAlias, ""),
|
|
745
|
+
});
|
|
594
746
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (existingFrom instanceof TableBuilder) {
|
|
617
|
-
currentTable = existingFrom;
|
|
747
|
+
/**
|
|
748
|
+
* Combine multiple Queryables with UNION (remove duplicates)
|
|
749
|
+
*
|
|
750
|
+
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
751
|
+
* @returns UNION-ed Queryable
|
|
752
|
+
* @throws If less than 2 queryables are passed
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```typescript
|
|
756
|
+
* const combined = Queryable.union(
|
|
757
|
+
* db.user().where((u) => [expr.eq(u.type, "admin")]),
|
|
758
|
+
* db.user().where((u) => [expr.eq(u.type, "manager")]),
|
|
759
|
+
* );
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
static union(...queries) {
|
|
763
|
+
if (queries.length < 2) {
|
|
764
|
+
throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
|
|
765
|
+
provided: queries.length,
|
|
766
|
+
minimum: 2,
|
|
767
|
+
});
|
|
618
768
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const targetTable = relationDef.meta.targetFn();
|
|
627
|
-
const fkColKeys = relationDef.meta.columns;
|
|
628
|
-
const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
|
|
629
|
-
result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
|
|
630
|
-
const qr = joinQr.from(targetTable);
|
|
631
|
-
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
632
|
-
const srcCols = Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw;
|
|
633
|
-
const conditions = [];
|
|
634
|
-
for (let i = 0; i < fkColKeys.length; i++) {
|
|
635
|
-
const fkCol = srcCols[fkColKeys[i]];
|
|
636
|
-
const pkCol = qr.meta.columns[targetPkColKeys[i]];
|
|
637
|
-
conditions.push(expr.eq(pkCol, fkCol));
|
|
638
|
-
}
|
|
639
|
-
return qr.where(() => conditions);
|
|
769
|
+
const first = queries[0];
|
|
770
|
+
const unionAlias = first.meta.db.getNextAlias();
|
|
771
|
+
return new Queryable({
|
|
772
|
+
db: first.meta.db,
|
|
773
|
+
from: queries, // Queryable[] 배열로 저장
|
|
774
|
+
as: unionAlias,
|
|
775
|
+
columns: transformColumnsAlias(first.meta.columns, unionAlias, ""),
|
|
640
776
|
});
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region ========== recursive - WITH RECURSIVE ==========
|
|
780
|
+
/**
|
|
781
|
+
* Generate a recursive CTE (Common Table Expression)
|
|
782
|
+
*
|
|
783
|
+
* Used for querying hierarchical data (org charts, category trees, etc.)
|
|
784
|
+
*
|
|
785
|
+
* @param fn - Callback function that defines the recursive part
|
|
786
|
+
* @returns Queryable with the recursive CTE applied
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* ```typescript
|
|
790
|
+
* // Query org chart hierarchy
|
|
791
|
+
* db.employee()
|
|
792
|
+
* .where((e) => [expr.null(e.managerId)]) // Root nodes
|
|
793
|
+
* .recursive((cte) =>
|
|
794
|
+
* cte.from(Employee)
|
|
795
|
+
* .where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
796
|
+
* )
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
799
|
+
recursive(fn) {
|
|
800
|
+
if (Array.isArray(this.meta.from)) {
|
|
801
|
+
const newFroms = this.meta.from.map((from) => from.recursive(fn));
|
|
802
|
+
return new Queryable({
|
|
803
|
+
...this.meta,
|
|
804
|
+
from: newFroms,
|
|
805
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
806
|
+
});
|
|
650
807
|
}
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
currentTable = sourceTable;
|
|
669
|
-
}
|
|
808
|
+
// Generate dynamic CTE name
|
|
809
|
+
const cteName = this.meta.db.getNextAlias();
|
|
810
|
+
// 2. Transform target to Queryable (pass CTE name)
|
|
811
|
+
const cteQr = new RecursiveQueryable(this, cteName);
|
|
812
|
+
// 3. Execute fn (returns Queryable with conditions like where added)
|
|
813
|
+
const resultQr = fn(cteQr);
|
|
814
|
+
return new Queryable({
|
|
815
|
+
db: this.meta.db,
|
|
816
|
+
as: this.meta.as,
|
|
817
|
+
from: cteName,
|
|
818
|
+
columns: transformColumnsAlias(this.meta.columns, this.meta.as, ""),
|
|
819
|
+
with: {
|
|
820
|
+
name: cteName,
|
|
821
|
+
base: this, // Block circular reference type inference
|
|
822
|
+
recursive: resultQr,
|
|
823
|
+
},
|
|
824
|
+
});
|
|
670
825
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
* .wrap()
|
|
689
|
-
* .count();
|
|
690
|
-
* ```
|
|
691
|
-
*/
|
|
692
|
-
wrap() {
|
|
693
|
-
const wrapAlias = this.meta.db.getNextAlias();
|
|
694
|
-
return new Queryable({
|
|
695
|
-
db: this.meta.db,
|
|
696
|
-
from: this,
|
|
697
|
-
as: wrapAlias,
|
|
698
|
-
columns: transformColumnsAlias(this.meta.columns, wrapAlias, "")
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Combine multiple Queryables with UNION (remove duplicates)
|
|
703
|
-
*
|
|
704
|
-
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
705
|
-
* @returns UNION-ed Queryable
|
|
706
|
-
* @throws If less than 2 queryables are passed
|
|
707
|
-
*
|
|
708
|
-
* @example
|
|
709
|
-
* ```typescript
|
|
710
|
-
* const combined = Queryable.union(
|
|
711
|
-
* db.user().where((u) => [expr.eq(u.type, "admin")]),
|
|
712
|
-
* db.user().where((u) => [expr.eq(u.type, "manager")]),
|
|
713
|
-
* );
|
|
714
|
-
* ```
|
|
715
|
-
*/
|
|
716
|
-
static union(...queries) {
|
|
717
|
-
if (queries.length < 2) {
|
|
718
|
-
throw new ArgumentError("union requires at least 2 queryables.", {
|
|
719
|
-
provided: queries.length,
|
|
720
|
-
minimum: 2
|
|
721
|
-
});
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region ========== [query] Select - SELECT ==========
|
|
828
|
+
/**
|
|
829
|
+
* Execute a SELECT query and return the result array
|
|
830
|
+
*
|
|
831
|
+
* @returns Query result array
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```typescript
|
|
835
|
+
* const users = await db.user()
|
|
836
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
837
|
+
* .execute();
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
async execute() {
|
|
841
|
+
const results = await this.meta.db.executeDefs([this.getSelectQueryDef()], [this.getResultMeta()]);
|
|
842
|
+
return results[0];
|
|
722
843
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
* // Query org chart hierarchy
|
|
746
|
-
* db.employee()
|
|
747
|
-
* .where((e) => [expr.null(e.managerId)]) // Root nodes
|
|
748
|
-
* .recursive((cte) =>
|
|
749
|
-
* cte.from(Employee)
|
|
750
|
-
* .where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
751
|
-
* )
|
|
752
|
-
* ```
|
|
753
|
-
*/
|
|
754
|
-
recursive(fn) {
|
|
755
|
-
if (Array.isArray(this.meta.from)) {
|
|
756
|
-
const newFroms = this.meta.from.map((from) => from.recursive(fn));
|
|
757
|
-
return new Queryable({
|
|
758
|
-
...this.meta,
|
|
759
|
-
from: newFroms,
|
|
760
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
|
|
761
|
-
});
|
|
844
|
+
/**
|
|
845
|
+
* Return a single result (Error if more than 1)
|
|
846
|
+
*
|
|
847
|
+
* @returns Single result or undefined
|
|
848
|
+
* @throws When more than one result is returned
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* const user = await db.user()
|
|
853
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
854
|
+
* .single();
|
|
855
|
+
* ```
|
|
856
|
+
*/
|
|
857
|
+
async single() {
|
|
858
|
+
const result = await this.top(2).execute();
|
|
859
|
+
if (result.length > 1) {
|
|
860
|
+
throw new ArgumentError("단일 결과를 기대했으나 복수 결과가 반환되었습니다.", {
|
|
861
|
+
table: this._getSourceName(),
|
|
862
|
+
resultCount: result.length,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return result[0];
|
|
762
866
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
recursive: resultQr
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
//#endregion
|
|
780
|
-
//#region ========== [query] Select - SELECT ==========
|
|
781
|
-
/**
|
|
782
|
-
* Execute a SELECT query and return the result array
|
|
783
|
-
*
|
|
784
|
-
* @returns Query result array
|
|
785
|
-
*
|
|
786
|
-
* @example
|
|
787
|
-
* ```typescript
|
|
788
|
-
* const users = await db.user()
|
|
789
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
790
|
-
* .execute();
|
|
791
|
-
* ```
|
|
792
|
-
*/
|
|
793
|
-
async execute() {
|
|
794
|
-
const results = await this.meta.db.executeDefs(
|
|
795
|
-
[this.getSelectQueryDef()],
|
|
796
|
-
[this.getResultMeta()]
|
|
797
|
-
);
|
|
798
|
-
return results[0];
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Return a single result (Error if more than 1)
|
|
802
|
-
*
|
|
803
|
-
* @returns Single result or undefined
|
|
804
|
-
* @throws When more than one result is returned
|
|
805
|
-
*
|
|
806
|
-
* @example
|
|
807
|
-
* ```typescript
|
|
808
|
-
* const user = await db.user()
|
|
809
|
-
* .where((u) => [expr.eq(u.id, 1)])
|
|
810
|
-
* .single();
|
|
811
|
-
* ```
|
|
812
|
-
*/
|
|
813
|
-
async single() {
|
|
814
|
-
const result = await this.top(2).execute();
|
|
815
|
-
if (result.length > 1) {
|
|
816
|
-
throw new ArgumentError("Expected single result but multiple results returned.", {
|
|
817
|
-
table: this._getSourceName(),
|
|
818
|
-
resultCount: result.length
|
|
819
|
-
});
|
|
867
|
+
/**
|
|
868
|
+
* Query 소스 이름 반환 (에러 메시지용)
|
|
869
|
+
*/
|
|
870
|
+
_getSourceName() {
|
|
871
|
+
const from = this.meta.from;
|
|
872
|
+
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
873
|
+
return from.meta.name;
|
|
874
|
+
}
|
|
875
|
+
if (typeof from === "string") {
|
|
876
|
+
return from;
|
|
877
|
+
}
|
|
878
|
+
return this.meta.as;
|
|
820
879
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
880
|
+
/**
|
|
881
|
+
* Return the first result (only the first even if multiple exist)
|
|
882
|
+
*
|
|
883
|
+
* @returns First result or undefined
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* ```typescript
|
|
887
|
+
* const latestUser = await db.user()
|
|
888
|
+
* .orderBy((u) => u.createdAt, "DESC")
|
|
889
|
+
* .first();
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
892
|
+
async first() {
|
|
893
|
+
const results = await this.top(1).execute();
|
|
894
|
+
return results[0];
|
|
830
895
|
}
|
|
831
|
-
|
|
832
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Return the number of result rows
|
|
898
|
+
*
|
|
899
|
+
* @param fn - Function to specify the column to count (optional)
|
|
900
|
+
* @returns Number of rows
|
|
901
|
+
* @throws Error when called directly after distinct() or groupBy() (use wrap() first)
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* ```typescript
|
|
905
|
+
* const count = await db.user()
|
|
906
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
907
|
+
* .count();
|
|
908
|
+
* ```
|
|
909
|
+
*/
|
|
910
|
+
async count(fn) {
|
|
911
|
+
if (this.meta.distinct) {
|
|
912
|
+
throw new Error("distinct() 이후에 count()를 사용할 수 없습니다. wrap()을 먼저 사용하세요.");
|
|
913
|
+
}
|
|
914
|
+
if (this.meta.groupBy) {
|
|
915
|
+
throw new Error("groupBy() 이후에 count()를 사용할 수 없습니다. wrap()을 먼저 사용하세요.");
|
|
916
|
+
}
|
|
917
|
+
const countQr = fn
|
|
918
|
+
? this.select((c) => ({ cnt: expr.count(fn(c)) }))
|
|
919
|
+
: this.select(() => ({ cnt: expr.count() }));
|
|
920
|
+
const result = await countQr.single();
|
|
921
|
+
return result?.cnt ?? 0;
|
|
833
922
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const results = await this.top(1).execute();
|
|
850
|
-
return results[0];
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Return the number of result rows
|
|
854
|
-
*
|
|
855
|
-
* @param fn - Function to specify the column to count (optional)
|
|
856
|
-
* @returns Number of rows
|
|
857
|
-
* @throws Error when called directly after distinct() or groupBy() (use wrap() first)
|
|
858
|
-
*
|
|
859
|
-
* @example
|
|
860
|
-
* ```typescript
|
|
861
|
-
* const count = await db.user()
|
|
862
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
863
|
-
* .count();
|
|
864
|
-
* ```
|
|
865
|
-
*/
|
|
866
|
-
async count(fn) {
|
|
867
|
-
if (this.meta.distinct) {
|
|
868
|
-
throw new Error("Cannot use count() after distinct(). Use wrap() first.");
|
|
923
|
+
/**
|
|
924
|
+
* Check whether data matching the conditions exists
|
|
925
|
+
*
|
|
926
|
+
* @returns true if exists, false otherwise
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```typescript
|
|
930
|
+
* const hasAdmin = await db.user()
|
|
931
|
+
* .where((u) => [expr.eq(u.role, "admin")])
|
|
932
|
+
* .exists();
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
async exists() {
|
|
936
|
+
const count = await this.count();
|
|
937
|
+
return count > 0;
|
|
869
938
|
}
|
|
870
|
-
|
|
871
|
-
|
|
939
|
+
getSelectQueryDef() {
|
|
940
|
+
return obj.clearUndefined({
|
|
941
|
+
type: "select",
|
|
942
|
+
from: this._buildFromDef(),
|
|
943
|
+
as: this.meta.as,
|
|
944
|
+
select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : undefined,
|
|
945
|
+
distinct: this.meta.distinct,
|
|
946
|
+
top: this.meta.top,
|
|
947
|
+
lock: this.meta.lock,
|
|
948
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
949
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
950
|
+
orderBy: this.meta.orderBy?.map((o) => (o[1] ? [o[0].expr, o[1]] : [o[0].expr])),
|
|
951
|
+
limit: this.meta.limit,
|
|
952
|
+
groupBy: this.meta.groupBy?.map((g) => g.expr),
|
|
953
|
+
having: this.meta.having?.map((w) => w.expr),
|
|
954
|
+
with: this.meta.with
|
|
955
|
+
? {
|
|
956
|
+
name: this.meta.with.name,
|
|
957
|
+
base: this.meta.with.base.getSelectQueryDef(),
|
|
958
|
+
recursive: this.meta.with.recursive.getSelectQueryDef(),
|
|
959
|
+
}
|
|
960
|
+
: undefined,
|
|
961
|
+
});
|
|
872
962
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
* .where((u) => [expr.eq(u.role, "admin")])
|
|
886
|
-
* .exists();
|
|
887
|
-
* ```
|
|
888
|
-
*/
|
|
889
|
-
async exists() {
|
|
890
|
-
const count = await this.count();
|
|
891
|
-
return count > 0;
|
|
892
|
-
}
|
|
893
|
-
getSelectQueryDef() {
|
|
894
|
-
var _a, _b, _c, _d;
|
|
895
|
-
return obj.clearUndefined({
|
|
896
|
-
type: "select",
|
|
897
|
-
from: this._buildFromDef(),
|
|
898
|
-
as: this.meta.as,
|
|
899
|
-
select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : void 0,
|
|
900
|
-
distinct: this.meta.distinct,
|
|
901
|
-
top: this.meta.top,
|
|
902
|
-
lock: this.meta.lock,
|
|
903
|
-
where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
|
|
904
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
|
|
905
|
-
orderBy: (_b = this.meta.orderBy) == null ? void 0 : _b.map((o) => o[1] ? [o[0].expr, o[1]] : [o[0].expr]),
|
|
906
|
-
limit: this.meta.limit,
|
|
907
|
-
groupBy: (_c = this.meta.groupBy) == null ? void 0 : _c.map((g) => g.expr),
|
|
908
|
-
having: (_d = this.meta.having) == null ? void 0 : _d.map((w) => w.expr),
|
|
909
|
-
with: this.meta.with ? {
|
|
910
|
-
name: this.meta.with.name,
|
|
911
|
-
base: this.meta.with.base.getSelectQueryDef(),
|
|
912
|
-
recursive: this.meta.with.recursive.getSelectQueryDef()
|
|
913
|
-
} : void 0
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
_buildFromDef() {
|
|
917
|
-
const from = this.meta.from;
|
|
918
|
-
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
919
|
-
return this.meta.db.getQueryDefObjectName(from);
|
|
920
|
-
} else if (from instanceof Queryable) {
|
|
921
|
-
return from.getSelectQueryDef();
|
|
922
|
-
} else if (Array.isArray(from)) {
|
|
923
|
-
return from.map((qr) => qr.getSelectQueryDef());
|
|
963
|
+
_buildFromDef() {
|
|
964
|
+
const from = this.meta.from;
|
|
965
|
+
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
966
|
+
return this.meta.db.getQueryDefObjectName(from);
|
|
967
|
+
}
|
|
968
|
+
else if (from instanceof Queryable) {
|
|
969
|
+
return from.getSelectQueryDef();
|
|
970
|
+
}
|
|
971
|
+
else if (Array.isArray(from)) {
|
|
972
|
+
return from.map((qr) => qr.getSelectQueryDef());
|
|
973
|
+
}
|
|
974
|
+
return from;
|
|
924
975
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
976
|
+
_buildSelectDef(columns, prefix) {
|
|
977
|
+
const result = {};
|
|
978
|
+
for (const [key, val] of Object.entries(columns)) {
|
|
979
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
980
|
+
if (val instanceof ExprUnit) {
|
|
981
|
+
result[fullKey] = val.expr;
|
|
982
|
+
}
|
|
983
|
+
else if (Array.isArray(val)) {
|
|
984
|
+
if (val.length > 0) {
|
|
985
|
+
Object.assign(result, this._buildSelectDef(val[0], fullKey));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else if (typeof val === "object" && val != null) {
|
|
989
|
+
Object.assign(result, this._buildSelectDef(val, fullKey));
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
// 일반 값 (string, number, boolean 등) — Expr로 변환
|
|
993
|
+
result[fullKey] = expr.toExpr(val);
|
|
994
|
+
}
|
|
936
995
|
}
|
|
937
|
-
|
|
938
|
-
Object.assign(result, this._buildSelectDef(val, fullKey));
|
|
939
|
-
} else {
|
|
940
|
-
result[fullKey] = expr.toExpr(val);
|
|
941
|
-
}
|
|
996
|
+
return result;
|
|
942
997
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
998
|
+
_buildJoinDefs(joins) {
|
|
999
|
+
const result = [];
|
|
1000
|
+
for (const join of joins) {
|
|
1001
|
+
const joinQr = join.queryable;
|
|
1002
|
+
const selectDef = joinQr.getSelectQueryDef();
|
|
1003
|
+
const joinDef = {
|
|
1004
|
+
...selectDef,
|
|
1005
|
+
as: joinQr.meta.as,
|
|
1006
|
+
isSingle: join.isSingle,
|
|
1007
|
+
};
|
|
1008
|
+
result.push(joinDef);
|
|
1009
|
+
}
|
|
1010
|
+
return result;
|
|
956
1011
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1012
|
+
getResultMeta(outputColumns) {
|
|
1013
|
+
const columns = {};
|
|
1014
|
+
const joins = {};
|
|
1015
|
+
const buildResultMeta = (cols, prefix) => {
|
|
1016
|
+
for (const [key, val] of Object.entries(cols)) {
|
|
1017
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1018
|
+
if (outputColumns && !outputColumns.includes(fullKey))
|
|
1019
|
+
continue;
|
|
1020
|
+
if (val instanceof ExprUnit) {
|
|
1021
|
+
// 원시 column
|
|
1022
|
+
columns[fullKey] = val.dataType;
|
|
1023
|
+
}
|
|
1024
|
+
else if (Array.isArray(val)) {
|
|
1025
|
+
// 배열 (1:N 관계)
|
|
1026
|
+
if (val.length > 0) {
|
|
1027
|
+
joins[fullKey] = { isSingle: false };
|
|
1028
|
+
buildResultMeta(val[0], fullKey);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else if (typeof val === "object") {
|
|
1032
|
+
// 단일 객체 (N:1, 1:1 관계)
|
|
1033
|
+
joins[fullKey] = { isSingle: true };
|
|
1034
|
+
buildResultMeta(val, fullKey);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
buildResultMeta(this.meta.columns, "");
|
|
1039
|
+
return { columns, joins };
|
|
1040
|
+
}
|
|
1041
|
+
async insert(records, outputColumns) {
|
|
1042
|
+
if (records.length === 0) {
|
|
1043
|
+
return outputColumns ? [] : undefined;
|
|
1044
|
+
}
|
|
1045
|
+
// MSSQL의 1000행 제한을 위해 청크로 분할
|
|
1046
|
+
const CHUNK_SIZE = 1000;
|
|
1047
|
+
const allResults = [];
|
|
1048
|
+
for (let i = 0; i < records.length; i += CHUNK_SIZE) {
|
|
1049
|
+
const chunk = records.slice(i, i + CHUNK_SIZE);
|
|
1050
|
+
const results = await this.meta.db.executeDefs([this.getInsertQueryDef(chunk, outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
|
|
1051
|
+
if (outputColumns) {
|
|
1052
|
+
allResults.push(...results[0]);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (outputColumns) {
|
|
1056
|
+
return allResults;
|
|
976
1057
|
}
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
buildResultMeta(this.meta.columns, "");
|
|
980
|
-
return { columns, joins };
|
|
981
|
-
}
|
|
982
|
-
async insert(records, outputColumns) {
|
|
983
|
-
if (records.length === 0) {
|
|
984
|
-
return outputColumns ? [] : void 0;
|
|
985
1058
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
[this.getInsertQueryDef(chunk, outputColumns)],
|
|
992
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : void 0
|
|
993
|
-
);
|
|
994
|
-
if (outputColumns) {
|
|
995
|
-
allResults.push(...results[0]);
|
|
996
|
-
}
|
|
1059
|
+
async insertIfNotExists(record, outputColumns) {
|
|
1060
|
+
const results = await this.meta.db.executeDefs([this.getInsertIfNotExistsQueryDef(record)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
|
|
1061
|
+
if (outputColumns) {
|
|
1062
|
+
return results[0][0];
|
|
1063
|
+
}
|
|
997
1064
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1065
|
+
async insertInto(targetTable, outputColumns) {
|
|
1066
|
+
const results = await this.meta.db.executeDefs([this.getInsertIntoQueryDef(targetTable)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
|
|
1067
|
+
if (outputColumns) {
|
|
1068
|
+
return results[0];
|
|
1069
|
+
}
|
|
1000
1070
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1071
|
+
getInsertQueryDef(records, outputColumns) {
|
|
1072
|
+
const from = this.meta.from;
|
|
1073
|
+
const outputDef = this._getCudOutputDef();
|
|
1074
|
+
// AI column에 명시적 값이 있으면 overrideIdentity 설정
|
|
1075
|
+
const overrideIdentity = outputDef.aiColName != null &&
|
|
1076
|
+
records.some((r) => r[outputDef.aiColName] !== undefined);
|
|
1077
|
+
return obj.clearUndefined({
|
|
1078
|
+
type: "insert",
|
|
1079
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1080
|
+
records,
|
|
1081
|
+
overrideIdentity: overrideIdentity || undefined,
|
|
1082
|
+
output: outputColumns
|
|
1083
|
+
? {
|
|
1084
|
+
columns: outputColumns,
|
|
1085
|
+
pkColNames: outputDef.pkColNames,
|
|
1086
|
+
aiColName: outputDef.aiColName,
|
|
1087
|
+
}
|
|
1088
|
+
: undefined,
|
|
1089
|
+
});
|
|
1009
1090
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1091
|
+
getInsertIfNotExistsQueryDef(record, outputColumns) {
|
|
1092
|
+
const from = this.meta.from;
|
|
1093
|
+
const outputDef = this._getCudOutputDef();
|
|
1094
|
+
const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1095
|
+
return obj.clearUndefined({
|
|
1096
|
+
type: "insertIfNotExists",
|
|
1097
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1098
|
+
record,
|
|
1099
|
+
existsSelectQuery,
|
|
1100
|
+
output: outputColumns
|
|
1101
|
+
? {
|
|
1102
|
+
columns: outputColumns,
|
|
1103
|
+
pkColNames: outputDef.pkColNames,
|
|
1104
|
+
aiColName: outputDef.aiColName,
|
|
1105
|
+
}
|
|
1106
|
+
: undefined,
|
|
1107
|
+
});
|
|
1018
1108
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
} : void 0
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
getInsertIfNotExistsQueryDef(record, outputColumns) {
|
|
1037
|
-
const from = this.meta.from;
|
|
1038
|
-
const outputDef = this._getCudOutputDef();
|
|
1039
|
-
const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1040
|
-
return obj.clearUndefined({
|
|
1041
|
-
type: "insertIfNotExists",
|
|
1042
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1043
|
-
record,
|
|
1044
|
-
existsSelectQuery,
|
|
1045
|
-
output: outputColumns ? {
|
|
1046
|
-
columns: outputColumns,
|
|
1047
|
-
pkColNames: outputDef.pkColNames,
|
|
1048
|
-
aiColName: outputDef.aiColName
|
|
1049
|
-
} : void 0
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
getInsertIntoQueryDef(targetTable, outputColumns) {
|
|
1053
|
-
const outputDef = this._getCudOutputDef();
|
|
1054
|
-
return obj.clearUndefined({
|
|
1055
|
-
type: "insertInto",
|
|
1056
|
-
table: this.meta.db.getQueryDefObjectName(targetTable),
|
|
1057
|
-
recordsSelectQuery: this.getSelectQueryDef(),
|
|
1058
|
-
output: outputColumns ? {
|
|
1059
|
-
columns: outputColumns,
|
|
1060
|
-
pkColNames: outputDef.pkColNames,
|
|
1061
|
-
aiColName: outputDef.aiColName
|
|
1062
|
-
} : void 0
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
async update(recordFwd, outputColumns) {
|
|
1066
|
-
const results = await this.meta.db.executeDefs(
|
|
1067
|
-
[this.getUpdateQueryDef(recordFwd, outputColumns)],
|
|
1068
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : void 0
|
|
1069
|
-
);
|
|
1070
|
-
if (outputColumns) {
|
|
1071
|
-
return results[0];
|
|
1109
|
+
getInsertIntoQueryDef(targetTable, outputColumns) {
|
|
1110
|
+
const outputDef = this._getCudOutputDef();
|
|
1111
|
+
return obj.clearUndefined({
|
|
1112
|
+
type: "insertInto",
|
|
1113
|
+
table: this.meta.db.getQueryDefObjectName(targetTable),
|
|
1114
|
+
recordsSelectQuery: this.getSelectQueryDef(),
|
|
1115
|
+
output: outputColumns
|
|
1116
|
+
? {
|
|
1117
|
+
columns: outputColumns,
|
|
1118
|
+
pkColNames: outputDef.pkColNames,
|
|
1119
|
+
aiColName: outputDef.aiColName,
|
|
1120
|
+
}
|
|
1121
|
+
: undefined,
|
|
1122
|
+
});
|
|
1072
1123
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
);
|
|
1079
|
-
if (outputColumns) {
|
|
1080
|
-
return results[0];
|
|
1124
|
+
async update(recordFwd, outputColumns) {
|
|
1125
|
+
const results = await this.meta.db.executeDefs([this.getUpdateQueryDef(recordFwd, outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
|
|
1126
|
+
if (outputColumns) {
|
|
1127
|
+
return results[0];
|
|
1128
|
+
}
|
|
1081
1129
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
return obj.clearUndefined({
|
|
1088
|
-
type: "update",
|
|
1089
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1090
|
-
as: this.meta.as,
|
|
1091
|
-
record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
|
|
1092
|
-
top: this.meta.top,
|
|
1093
|
-
where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
|
|
1094
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
|
|
1095
|
-
limit: this.meta.limit,
|
|
1096
|
-
output: outputColumns ? {
|
|
1097
|
-
columns: outputColumns,
|
|
1098
|
-
pkColNames: outputDef.pkColNames,
|
|
1099
|
-
aiColName: outputDef.aiColName
|
|
1100
|
-
} : void 0
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
getDeleteQueryDef(outputColumns) {
|
|
1104
|
-
var _a;
|
|
1105
|
-
const from = this.meta.from;
|
|
1106
|
-
const outputDef = this._getCudOutputDef();
|
|
1107
|
-
return obj.clearUndefined({
|
|
1108
|
-
type: "delete",
|
|
1109
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1110
|
-
as: this.meta.as,
|
|
1111
|
-
top: this.meta.top,
|
|
1112
|
-
where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
|
|
1113
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
|
|
1114
|
-
limit: this.meta.limit,
|
|
1115
|
-
output: outputColumns ? {
|
|
1116
|
-
columns: outputColumns,
|
|
1117
|
-
pkColNames: outputDef.pkColNames,
|
|
1118
|
-
aiColName: outputDef.aiColName
|
|
1119
|
-
} : void 0
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
async upsert(updateFnOrInsertFn, insertFnOrOutputColumns, outputColumns) {
|
|
1123
|
-
const updateRecordFn = updateFnOrInsertFn;
|
|
1124
|
-
const insertRecordFn = insertFnOrOutputColumns instanceof Function ? insertFnOrOutputColumns : updateFnOrInsertFn;
|
|
1125
|
-
const realOutputColumns = insertFnOrOutputColumns instanceof Function ? outputColumns : insertFnOrOutputColumns;
|
|
1126
|
-
const results = await this.meta.db.executeDefs(
|
|
1127
|
-
[this.getUpsertQueryDef(updateRecordFn, insertRecordFn, realOutputColumns)],
|
|
1128
|
-
[realOutputColumns ? this.getResultMeta(realOutputColumns) : void 0]
|
|
1129
|
-
);
|
|
1130
|
-
if (realOutputColumns) {
|
|
1131
|
-
return results[0];
|
|
1130
|
+
async delete(outputColumns) {
|
|
1131
|
+
const results = await this.meta.db.executeDefs([this.getDeleteQueryDef(outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
|
|
1132
|
+
if (outputColumns) {
|
|
1133
|
+
return results[0];
|
|
1134
|
+
}
|
|
1132
1135
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1136
|
+
getUpdateQueryDef(recordFwd, outputColumns) {
|
|
1137
|
+
const from = this.meta.from;
|
|
1138
|
+
const outputDef = this._getCudOutputDef();
|
|
1139
|
+
return obj.clearUndefined({
|
|
1140
|
+
type: "update",
|
|
1141
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1142
|
+
as: this.meta.as,
|
|
1143
|
+
record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
|
|
1144
|
+
top: this.meta.top,
|
|
1145
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
1146
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1147
|
+
limit: this.meta.limit,
|
|
1148
|
+
output: outputColumns
|
|
1149
|
+
? {
|
|
1150
|
+
columns: outputColumns,
|
|
1151
|
+
pkColNames: outputDef.pkColNames,
|
|
1152
|
+
aiColName: outputDef.aiColName,
|
|
1153
|
+
}
|
|
1154
|
+
: undefined,
|
|
1155
|
+
});
|
|
1142
1156
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1157
|
+
getDeleteQueryDef(outputColumns) {
|
|
1158
|
+
const from = this.meta.from;
|
|
1159
|
+
const outputDef = this._getCudOutputDef();
|
|
1160
|
+
return obj.clearUndefined({
|
|
1161
|
+
type: "delete",
|
|
1162
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1163
|
+
as: this.meta.as,
|
|
1164
|
+
top: this.meta.top,
|
|
1165
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
1166
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1167
|
+
limit: this.meta.limit,
|
|
1168
|
+
output: outputColumns
|
|
1169
|
+
? {
|
|
1170
|
+
columns: outputColumns,
|
|
1171
|
+
pkColNames: outputDef.pkColNames,
|
|
1172
|
+
aiColName: outputDef.aiColName,
|
|
1173
|
+
}
|
|
1174
|
+
: undefined,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
async upsert(updateFnOrInsertFn, insertFnOrOutputColumns, outputColumns) {
|
|
1178
|
+
const updateRecordFn = updateFnOrInsertFn;
|
|
1179
|
+
const insertRecordFn = (insertFnOrOutputColumns instanceof Function ? insertFnOrOutputColumns : updateFnOrInsertFn);
|
|
1180
|
+
const realOutputColumns = insertFnOrOutputColumns instanceof Function ? outputColumns : insertFnOrOutputColumns;
|
|
1181
|
+
const results = await this.meta.db.executeDefs([this.getUpsertQueryDef(updateRecordFn, insertRecordFn, realOutputColumns)], [realOutputColumns ? this.getResultMeta(realOutputColumns) : undefined]);
|
|
1182
|
+
if (realOutputColumns) {
|
|
1183
|
+
return results[0];
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
getUpsertQueryDef(updateRecordFn, insertRecordFn, outputColumns) {
|
|
1187
|
+
const from = this.meta.from;
|
|
1188
|
+
const outputDef = this._getCudOutputDef();
|
|
1189
|
+
const { select: _sel, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1190
|
+
// updateRecord 생성
|
|
1191
|
+
const updateQrRecord = updateRecordFn(this.meta.columns);
|
|
1192
|
+
const updateRecord = {};
|
|
1193
|
+
for (const [key, value] of Object.entries(updateQrRecord)) {
|
|
1194
|
+
updateRecord[key] = expr.toExpr(value);
|
|
1195
|
+
}
|
|
1196
|
+
// insertRecord 생성 (updateRecordRaw를 두 번째 인자로 전달)
|
|
1197
|
+
const insertRecordRaw = insertRecordFn(updateQrRecord);
|
|
1198
|
+
const insertRecord = Object.fromEntries(Object.entries(insertRecordRaw).map(([key, value]) => [key, expr.toExpr(value)]));
|
|
1199
|
+
return obj.clearUndefined({
|
|
1200
|
+
type: "upsert",
|
|
1201
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1202
|
+
existsSelectQuery,
|
|
1203
|
+
updateRecord,
|
|
1204
|
+
insertRecord,
|
|
1205
|
+
output: outputColumns
|
|
1206
|
+
? {
|
|
1207
|
+
columns: outputColumns,
|
|
1208
|
+
pkColNames: outputDef.pkColNames,
|
|
1209
|
+
aiColName: outputDef.aiColName,
|
|
1210
|
+
}
|
|
1211
|
+
: undefined,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region ========== DDL Helper ==========
|
|
1216
|
+
/**
|
|
1217
|
+
* FK 제약조건 활성화/비활성화 (트랜잭션 내에서 사용 가능)
|
|
1218
|
+
*/
|
|
1219
|
+
async switchFk(enabled) {
|
|
1220
|
+
const from = this.meta.from;
|
|
1221
|
+
if (!(from instanceof TableBuilder) && !(from instanceof ViewBuilder)) {
|
|
1222
|
+
throw new Error("switchFk는 TableBuilder 또는 ViewBuilder 기반 queryable에서만 사용할 수 있습니다.");
|
|
1223
|
+
}
|
|
1224
|
+
await this.meta.db.switchFk(this.meta.db.getQueryDefObjectName(from), enabled);
|
|
1171
1225
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region ========== CUD Common ==========
|
|
1228
|
+
_getCudOutputDef() {
|
|
1229
|
+
const from = this.meta.from;
|
|
1230
|
+
if (from instanceof TableBuilder) {
|
|
1231
|
+
if (from.meta.columns == null) {
|
|
1232
|
+
throw new Error(`테이블 '${from.meta.name}'에 Column 정의가 없습니다.`);
|
|
1233
|
+
}
|
|
1234
|
+
let aiColName;
|
|
1235
|
+
for (const [key, col] of Object.entries(from.meta.columns)) {
|
|
1236
|
+
if (col.meta.autoIncrement) {
|
|
1237
|
+
aiColName = key;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
pkColNames: from.meta.primaryKey ?? [],
|
|
1242
|
+
aiColName,
|
|
1243
|
+
};
|
|
1186
1244
|
}
|
|
1187
|
-
|
|
1188
|
-
return {
|
|
1189
|
-
pkColNames: from.meta.primaryKey ?? [],
|
|
1190
|
-
aiColName
|
|
1191
|
-
};
|
|
1245
|
+
throw new Error("CUD 작업은 TableBuilder 기반 queryable에서만 사용할 수 있습니다.");
|
|
1192
1246
|
}
|
|
1193
|
-
throw new Error("CUD operations can only be used on TableBuilder-based queryables.");
|
|
1194
|
-
}
|
|
1195
|
-
//#endregion
|
|
1196
1247
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1248
|
+
//#region ========== Helper Functions ==========
|
|
1249
|
+
/**
|
|
1250
|
+
* Match FK column array with the target Table's PK and return PK column name array
|
|
1251
|
+
*
|
|
1252
|
+
* @param fkCols - FK column name array
|
|
1253
|
+
* @param targetTable - Target Table builder being referenced
|
|
1254
|
+
* @returns Matched PK column name array
|
|
1255
|
+
* @throws When FK/PK column count mismatch
|
|
1256
|
+
*/
|
|
1257
|
+
export function getMatchedPrimaryKeys(fkCols, targetTable) {
|
|
1258
|
+
const pk = targetTable.meta.primaryKey;
|
|
1259
|
+
if (pk == null || fkCols.length !== pk.length) {
|
|
1260
|
+
throw new Error(`FK/PK column count mismatch (target: ${targetTable.meta.name}, FK: ${fkCols.length}, PK: ${pk?.length ?? 0})`);
|
|
1261
|
+
}
|
|
1262
|
+
return pk;
|
|
1205
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Common helper to transform nested columns structure to a new alias
|
|
1266
|
+
*
|
|
1267
|
+
* When wrapping as Subquery/JOIN, transforms existing alias to new alias while
|
|
1268
|
+
* keeping nested keys (posts.userId) as flattened keys.
|
|
1269
|
+
*
|
|
1270
|
+
* e.g.: If the path of posts[0].userId column is ["T1.posts", "userId"],
|
|
1271
|
+
* transforming to new alias "T2" yields ["T2", "posts.userId"].
|
|
1272
|
+
*
|
|
1273
|
+
* @param columns - Column record to transform
|
|
1274
|
+
* @param alias - New Table alias (e.g., "T2")
|
|
1275
|
+
* @param keyPrefix - Current nested path (for recursive calls, default "")
|
|
1276
|
+
* @returns Transformed column record
|
|
1277
|
+
*/
|
|
1206
1278
|
function transformColumnsAlias(columns, alias, keyPrefix = "") {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1279
|
+
const result = {};
|
|
1280
|
+
for (const [key, value] of Object.entries(columns)) {
|
|
1281
|
+
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
|
|
1282
|
+
if (value instanceof ExprUnit) {
|
|
1283
|
+
result[key] = expr.col(value.dataType, alias, fullKey);
|
|
1284
|
+
}
|
|
1285
|
+
else if (Array.isArray(value)) {
|
|
1286
|
+
if (value.length > 0) {
|
|
1287
|
+
result[key] = [
|
|
1288
|
+
transformColumnsAlias(value[0], alias, fullKey),
|
|
1289
|
+
];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
else if (typeof value === "object" && value != null) {
|
|
1293
|
+
result[key] = transformColumnsAlias(value, alias, fullKey);
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
result[key] = value;
|
|
1297
|
+
}
|
|
1222
1298
|
}
|
|
1223
|
-
|
|
1224
|
-
return result;
|
|
1299
|
+
return result;
|
|
1225
1300
|
}
|
|
1226
|
-
const PATH_SYMBOL =
|
|
1301
|
+
const PATH_SYMBOL = Symbol("path");
|
|
1302
|
+
/**
|
|
1303
|
+
* PathProxy 인스턴스 생성
|
|
1304
|
+
* Proxy를 사용하여 속성 접근을 가로채고 경로를 수집
|
|
1305
|
+
*/
|
|
1227
1306
|
function createPathProxy(path = []) {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1307
|
+
return new Proxy({}, {
|
|
1308
|
+
get(_, prop) {
|
|
1309
|
+
if (prop === PATH_SYMBOL)
|
|
1310
|
+
return path;
|
|
1311
|
+
if (typeof prop === "symbol")
|
|
1312
|
+
return undefined;
|
|
1313
|
+
return createPathProxy([...path, prop]);
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1235
1316
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1317
|
+
//#endregion
|
|
1318
|
+
/**
|
|
1319
|
+
* Table 또는 View용 Queryable factory 함수 생성
|
|
1320
|
+
*
|
|
1321
|
+
* DbContext에서 Table/View별 getter를 정의할 때 사용
|
|
1322
|
+
*
|
|
1323
|
+
* @param db - DbContext 인스턴스
|
|
1324
|
+
* @param tableOrView - TableBuilder 또는 ViewBuilder 인스턴스
|
|
1325
|
+
* @param as - Alias 지정 (선택, 미지정 시 자동 생성)
|
|
1326
|
+
* @returns Queryable을 반환하는 factory 함수
|
|
1327
|
+
*
|
|
1328
|
+
* @example
|
|
1329
|
+
* ```typescript
|
|
1330
|
+
* class AppDbContext extends DbContext {
|
|
1331
|
+
* // 호출할 때마다 새 alias가 할당됨
|
|
1332
|
+
* user = queryable(this, User);
|
|
1333
|
+
*
|
|
1334
|
+
* // 사용 예시
|
|
1335
|
+
* async getActiveUsers() {
|
|
1336
|
+
* return this.user()
|
|
1337
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1338
|
+
* .execute();
|
|
1339
|
+
* }
|
|
1340
|
+
* }
|
|
1341
|
+
* ```
|
|
1342
|
+
*/
|
|
1343
|
+
export function queryable(db, tableOrView, as) {
|
|
1344
|
+
return () => {
|
|
1345
|
+
// as가 미지정이면 db.getNextAlias() 사용 (카운터 증가)
|
|
1346
|
+
// as가 지정되면 그대로 사용 (카운터 증가 없음)
|
|
1347
|
+
const finalAs = as ?? db.getNextAlias();
|
|
1348
|
+
// TableBuilder + columns
|
|
1349
|
+
if (tableOrView instanceof TableBuilder && tableOrView.meta.columns != null) {
|
|
1350
|
+
const columnDefs = tableOrView.meta.columns;
|
|
1351
|
+
return new Queryable({
|
|
1352
|
+
db,
|
|
1353
|
+
from: tableOrView,
|
|
1354
|
+
as: finalAs,
|
|
1355
|
+
columns: Object.fromEntries(Object.entries(columnDefs).map(([key, colDef]) => [
|
|
1356
|
+
key,
|
|
1357
|
+
expr.col(colDef.meta.type, finalAs, key),
|
|
1358
|
+
])),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
// ViewBuilder + viewFn
|
|
1362
|
+
if (tableOrView instanceof ViewBuilder && tableOrView.meta.viewFn != null) {
|
|
1363
|
+
const baseQr = tableOrView.meta.viewFn(db);
|
|
1364
|
+
// TFrom을 ViewBuilder로 설정하여 반환
|
|
1365
|
+
return new Queryable({
|
|
1366
|
+
db,
|
|
1367
|
+
from: tableOrView,
|
|
1368
|
+
as: finalAs,
|
|
1369
|
+
columns: transformColumnsAlias(baseQr.meta.columns, finalAs),
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
throw new Error(`Invalid Table/View Metadata: ${tableOrView.meta.name}`);
|
|
1373
|
+
};
|
|
1264
1374
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
getMatchedPrimaryKeys,
|
|
1268
|
-
queryable
|
|
1269
|
-
};
|
|
1270
|
-
//# sourceMappingURL=queryable.js.map
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//# sourceMappingURL=queryable.js.map
|