metal-orm 1.1.3 → 1.1.5
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 +715 -703
- package/dist/index.cjs +655 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -8
- package/dist/index.d.ts +170 -8
- package/dist/index.js +649 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/render.mjs +24 -1
- package/scripts/naming-strategy.mjs +16 -1
- package/src/core/ast/procedure.ts +21 -0
- package/src/core/ast/query.ts +47 -19
- package/src/core/ddl/introspect/utils.ts +56 -56
- package/src/core/dialect/abstract.ts +560 -547
- package/src/core/dialect/base/sql-dialect.ts +43 -29
- package/src/core/dialect/mssql/index.ts +369 -232
- package/src/core/dialect/mysql/index.ts +99 -7
- package/src/core/dialect/postgres/index.ts +121 -60
- package/src/core/dialect/sqlite/index.ts +97 -64
- package/src/core/execution/db-executor.ts +108 -90
- package/src/core/execution/executors/mssql-executor.ts +28 -24
- package/src/core/execution/executors/mysql-executor.ts +62 -27
- package/src/core/execution/executors/sqlite-executor.ts +10 -9
- package/src/index.ts +9 -6
- package/src/orm/execute-procedure.ts +77 -0
- package/src/orm/execute.ts +74 -73
- package/src/orm/interceptor-pipeline.ts +21 -17
- package/src/orm/pooled-executor-factory.ts +41 -20
- package/src/orm/unit-of-work.ts +6 -4
- package/src/query/index.ts +8 -5
- package/src/query-builder/delete.ts +3 -2
- package/src/query-builder/insert-query-state.ts +47 -19
- package/src/query-builder/insert.ts +142 -28
- package/src/query-builder/procedure-call.ts +122 -0
- package/src/query-builder/select/select-operations.ts +5 -2
- package/src/query-builder/select.ts +1146 -1105
- package/src/query-builder/update.ts +3 -2
- package/src/tree/tree-manager.ts +754 -754
|
@@ -1,20 +1,111 @@
|
|
|
1
|
-
import type { SelectQueryBuilder } from './select.js';
|
|
2
|
-
import { TableDef } from '../schema/table.js';
|
|
3
|
-
import { ColumnDef } from '../schema/column-types.js';
|
|
4
|
-
import {
|
|
1
|
+
import type { SelectQueryBuilder } from './select.js';
|
|
2
|
+
import { TableDef } from '../schema/table.js';
|
|
3
|
+
import { ColumnDef } from '../schema/column-types.js';
|
|
4
|
+
import {
|
|
5
|
+
ColumnNode,
|
|
6
|
+
ExpressionNode,
|
|
7
|
+
isValueOperandInput,
|
|
8
|
+
valueToOperand
|
|
9
|
+
} from '../core/ast/expression.js';
|
|
5
10
|
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
6
|
-
import { CompiledQuery, InsertCompiler, Dialect } from '../core/dialect/abstract.js';
|
|
7
|
-
import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import { CompiledQuery, InsertCompiler, Dialect } from '../core/dialect/abstract.js';
|
|
12
|
+
import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
|
|
13
|
+
import {
|
|
14
|
+
InsertQueryNode,
|
|
15
|
+
SelectQueryNode,
|
|
16
|
+
UpsertClause,
|
|
17
|
+
UpdateAssignmentNode
|
|
18
|
+
} from '../core/ast/query.js';
|
|
19
|
+
import { InsertQueryState } from './insert-query-state.js';
|
|
20
|
+
import { buildColumnNode } from '../core/ast/builders.js';
|
|
21
|
+
|
|
22
|
+
type InsertDialectInput = Dialect | DialectKey;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builder returned by InsertQueryBuilder.onConflict()
|
|
26
|
+
*/
|
|
27
|
+
export class ConflictBuilder<T> {
|
|
28
|
+
private readonly table: TableDef;
|
|
29
|
+
private readonly columns: ColumnNode[];
|
|
30
|
+
private readonly constraint?: string;
|
|
31
|
+
private readonly applyClause: (clause: UpsertClause) => InsertQueryBuilder<T>;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
table: TableDef,
|
|
35
|
+
columns: ColumnNode[],
|
|
36
|
+
constraint: string | undefined,
|
|
37
|
+
applyClause: (clause: UpsertClause) => InsertQueryBuilder<T>
|
|
38
|
+
) {
|
|
39
|
+
this.table = table;
|
|
40
|
+
this.columns = columns;
|
|
41
|
+
this.constraint = constraint;
|
|
42
|
+
this.applyClause = applyClause;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Adds ON CONFLICT ... DO UPDATE
|
|
47
|
+
* @param set - Column assignments for update branch
|
|
48
|
+
* @param where - Optional filter for update branch
|
|
49
|
+
* @returns InsertQueryBuilder with the upsert clause configured
|
|
50
|
+
*/
|
|
51
|
+
doUpdate(
|
|
52
|
+
set: Record<string, ValueOperandInput>,
|
|
53
|
+
where?: ExpressionNode
|
|
54
|
+
): InsertQueryBuilder<T> {
|
|
55
|
+
const assignments = this.buildAssignments(set);
|
|
56
|
+
return this.applyClause({
|
|
57
|
+
target: this.buildTarget(),
|
|
58
|
+
action: {
|
|
59
|
+
type: 'DoUpdate',
|
|
60
|
+
set: assignments,
|
|
61
|
+
where
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Adds ON CONFLICT ... DO NOTHING
|
|
68
|
+
* @returns InsertQueryBuilder with the upsert clause configured
|
|
69
|
+
*/
|
|
70
|
+
doNothing(): InsertQueryBuilder<T> {
|
|
71
|
+
return this.applyClause({
|
|
72
|
+
target: this.buildTarget(),
|
|
73
|
+
action: { type: 'DoNothing' }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private buildTarget(): UpsertClause['target'] {
|
|
78
|
+
return {
|
|
79
|
+
columns: [...this.columns],
|
|
80
|
+
constraint: this.constraint
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private buildAssignments(set: Record<string, ValueOperandInput>): UpdateAssignmentNode[] {
|
|
85
|
+
const entries = Object.entries(set);
|
|
86
|
+
if (!entries.length) {
|
|
87
|
+
throw new Error('ON CONFLICT DO UPDATE requires at least one assignment.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return entries.map(([columnName, rawValue]) => {
|
|
91
|
+
if (!isValueOperandInput(rawValue)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Invalid upsert value for column "${columnName}": only string, number, boolean, Date, Buffer, null, or OperandNodes are allowed`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
column: buildColumnNode(this.table, { name: columnName, table: this.table.name }),
|
|
99
|
+
value: valueToOperand(rawValue)
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builder for INSERT queries
|
|
107
|
+
*/
|
|
108
|
+
export class InsertQueryBuilder<T> {
|
|
18
109
|
private readonly table: TableDef;
|
|
19
110
|
private readonly state: InsertQueryState;
|
|
20
111
|
|
|
@@ -28,18 +119,22 @@ export class InsertQueryBuilder<T> {
|
|
|
28
119
|
this.state = state ?? new InsertQueryState(table);
|
|
29
120
|
}
|
|
30
121
|
|
|
31
|
-
private clone(state: InsertQueryState): InsertQueryBuilder<T> {
|
|
32
|
-
return new InsertQueryBuilder(this.table, state);
|
|
33
|
-
}
|
|
122
|
+
private clone(state: InsertQueryState): InsertQueryBuilder<T> {
|
|
123
|
+
return new InsertQueryBuilder(this.table, state);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private withOnConflict(clause: UpsertClause): InsertQueryBuilder<T> {
|
|
127
|
+
return this.clone(this.state.withOnConflict(clause));
|
|
128
|
+
}
|
|
34
129
|
|
|
35
130
|
/**
|
|
36
131
|
* Adds VALUES to the INSERT query
|
|
37
132
|
* @param rowOrRows - Single row object or array of row objects to insert
|
|
38
133
|
* @returns A new InsertQueryBuilder with the VALUES clause added
|
|
39
134
|
*/
|
|
40
|
-
values(
|
|
41
|
-
rowOrRows: Record<string, ValueOperandInput> | Record<string, ValueOperandInput>[]
|
|
42
|
-
): InsertQueryBuilder<T> {
|
|
135
|
+
values(
|
|
136
|
+
rowOrRows: Record<string, ValueOperandInput> | Record<string, ValueOperandInput>[]
|
|
137
|
+
): InsertQueryBuilder<T> {
|
|
43
138
|
const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows];
|
|
44
139
|
if (!rows.length) return this;
|
|
45
140
|
return this.clone(this.state.withValues(rows));
|
|
@@ -62,14 +157,33 @@ export class InsertQueryBuilder<T> {
|
|
|
62
157
|
* @param columns - Optional target columns for the INSERT
|
|
63
158
|
* @returns A new InsertQueryBuilder with the SELECT source
|
|
64
159
|
*/
|
|
65
|
-
fromSelect<TSource extends TableDef>(
|
|
66
|
-
query: SelectQueryNode | SelectQueryBuilder<unknown, TSource>,
|
|
67
|
-
columns: (ColumnDef | ColumnNode)[] = []
|
|
68
|
-
): InsertQueryBuilder<T> {
|
|
160
|
+
fromSelect<TSource extends TableDef>(
|
|
161
|
+
query: SelectQueryNode | SelectQueryBuilder<unknown, TSource>,
|
|
162
|
+
columns: (ColumnDef | ColumnNode)[] = []
|
|
163
|
+
): InsertQueryBuilder<T> {
|
|
69
164
|
const ast = this.resolveSelectQuery(query);
|
|
70
165
|
const nodes = columns.length ? this.resolveColumnNodes(columns) : [];
|
|
71
|
-
return this.clone(this.state.withSelect(ast, nodes));
|
|
72
|
-
}
|
|
166
|
+
return this.clone(this.state.withSelect(ast, nodes));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Configures UPSERT conflict handling for INSERT.
|
|
171
|
+
* @param columns - Conflict target columns (ignored by MySQL)
|
|
172
|
+
* @param constraint - Named unique/primary constraint (PostgreSQL only)
|
|
173
|
+
* @returns ConflictBuilder for selecting action (DO UPDATE / DO NOTHING)
|
|
174
|
+
*/
|
|
175
|
+
onConflict(
|
|
176
|
+
columns: (ColumnDef | ColumnNode)[] = [],
|
|
177
|
+
constraint?: string
|
|
178
|
+
): ConflictBuilder<T> {
|
|
179
|
+
const resolvedColumns = columns.length ? this.resolveColumnNodes(columns) : [];
|
|
180
|
+
return new ConflictBuilder(
|
|
181
|
+
this.table,
|
|
182
|
+
resolvedColumns,
|
|
183
|
+
constraint,
|
|
184
|
+
clause => this.withOnConflict(clause)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
73
187
|
|
|
74
188
|
/**
|
|
75
189
|
* Adds a RETURNING clause to the INSERT query
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ProcedureCallNode, ProcedureParamNode } from '../core/ast/procedure.js';
|
|
2
|
+
import type { CompiledProcedureCall, Dialect } from '../core/dialect/abstract.js';
|
|
3
|
+
import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
|
|
4
|
+
import { valueToOperand, ValueOperandInput } from '../core/ast/expression-builders.js';
|
|
5
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
6
|
+
import { executeProcedureAst, type ProcedureExecutionResult } from '../orm/execute-procedure.js';
|
|
7
|
+
|
|
8
|
+
type ProcedureDialectInput = Dialect | DialectKey;
|
|
9
|
+
|
|
10
|
+
export interface CallProcedureOptions {
|
|
11
|
+
schema?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProcedureOutOptions {
|
|
15
|
+
dbType?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cloneParam = (param: ProcedureParamNode): ProcedureParamNode => ({
|
|
19
|
+
...param,
|
|
20
|
+
value: param.value ? { ...param.value } : undefined
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export class ProcedureCallBuilder {
|
|
24
|
+
private readonly ast: ProcedureCallNode;
|
|
25
|
+
|
|
26
|
+
constructor(name: string, options?: CallProcedureOptions, ast?: ProcedureCallNode) {
|
|
27
|
+
this.ast = ast ?? {
|
|
28
|
+
type: 'ProcedureCall',
|
|
29
|
+
ref: {
|
|
30
|
+
name,
|
|
31
|
+
schema: options?.schema
|
|
32
|
+
},
|
|
33
|
+
params: []
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private clone(nextParams: ProcedureParamNode[]): ProcedureCallBuilder {
|
|
38
|
+
return new ProcedureCallBuilder(
|
|
39
|
+
this.ast.ref.name,
|
|
40
|
+
{ schema: this.ast.ref.schema },
|
|
41
|
+
{
|
|
42
|
+
...this.ast,
|
|
43
|
+
ref: { ...this.ast.ref },
|
|
44
|
+
params: nextParams
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
in(name: string, value: ValueOperandInput): ProcedureCallBuilder {
|
|
50
|
+
return this.clone([
|
|
51
|
+
...this.ast.params.map(cloneParam),
|
|
52
|
+
{
|
|
53
|
+
name,
|
|
54
|
+
direction: 'in',
|
|
55
|
+
value: valueToOperand(value)
|
|
56
|
+
}
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
out(name: string, options?: ProcedureOutOptions): ProcedureCallBuilder {
|
|
61
|
+
return this.clone([
|
|
62
|
+
...this.ast.params.map(cloneParam),
|
|
63
|
+
{
|
|
64
|
+
name,
|
|
65
|
+
direction: 'out',
|
|
66
|
+
dbType: options?.dbType
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
inOut(name: string, value: ValueOperandInput, options?: ProcedureOutOptions): ProcedureCallBuilder {
|
|
72
|
+
return this.clone([
|
|
73
|
+
...this.ast.params.map(cloneParam),
|
|
74
|
+
{
|
|
75
|
+
name,
|
|
76
|
+
direction: 'inout',
|
|
77
|
+
value: valueToOperand(value),
|
|
78
|
+
dbType: options?.dbType
|
|
79
|
+
}
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
compile(dialect: ProcedureDialectInput): CompiledProcedureCall {
|
|
84
|
+
const resolved = resolveDialectInput(dialect);
|
|
85
|
+
this.validateMssqlOutDbType(resolved);
|
|
86
|
+
return resolved.compileProcedureCall(this.getAST());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toSql(dialect: ProcedureDialectInput): string {
|
|
90
|
+
return this.compile(dialect).sql;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getAST(): ProcedureCallNode {
|
|
94
|
+
return {
|
|
95
|
+
...this.ast,
|
|
96
|
+
ref: { ...this.ast.ref },
|
|
97
|
+
params: this.ast.params.map(cloneParam)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async execute(session: OrmSession): Promise<ProcedureExecutionResult> {
|
|
102
|
+
this.validateMssqlOutDbType(session.getExecutionContext().dialect);
|
|
103
|
+
return executeProcedureAst(session, this.getAST());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private validateMssqlOutDbType(dialect: Dialect): void {
|
|
107
|
+
const isMssqlDialect = dialect.constructor.name === 'SqlServerDialect';
|
|
108
|
+
if (!isMssqlDialect) return;
|
|
109
|
+
|
|
110
|
+
for (const param of this.ast.params) {
|
|
111
|
+
const needsDbType = param.direction === 'out' || param.direction === 'inout';
|
|
112
|
+
if (needsDbType && !param.dbType) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`MSSQL requires "dbType" for procedure parameter "${param.name}" with direction "${param.direction}".`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const callProcedure = (name: string, options?: CallProcedureOptions): ProcedureCallBuilder =>
|
|
122
|
+
new ProcedureCallBuilder(name, options);
|
|
@@ -11,6 +11,7 @@ import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
|
|
|
11
11
|
import { OrmSession } from '../../orm/orm-session.js';
|
|
12
12
|
import type { SelectQueryBuilder } from '../select.js';
|
|
13
13
|
import { findPrimaryKey } from '../hydration-planner.js';
|
|
14
|
+
import { payloadResultSets } from '../../core/execution/db-executor.js';
|
|
14
15
|
|
|
15
16
|
export type WhereHasOptions = {
|
|
16
17
|
correlate?: ExpressionNode;
|
|
@@ -99,7 +100,8 @@ export async function executeCount(
|
|
|
99
100
|
|
|
100
101
|
const execCtx = session.getExecutionContext();
|
|
101
102
|
const compiled = execCtx.dialect.compileSelect(countQuery);
|
|
102
|
-
const
|
|
103
|
+
const payload = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
104
|
+
const results = payloadResultSets(payload);
|
|
103
105
|
const value = results[0]?.values?.[0]?.[0];
|
|
104
106
|
|
|
105
107
|
if (typeof value === 'number') return value;
|
|
@@ -145,7 +147,8 @@ export async function executeCountRows(
|
|
|
145
147
|
|
|
146
148
|
const execCtx = session.getExecutionContext();
|
|
147
149
|
const compiled = execCtx.dialect.compileSelect(countQuery);
|
|
148
|
-
const
|
|
150
|
+
const payload = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
151
|
+
const results = payloadResultSets(payload);
|
|
149
152
|
const value = results[0]?.values?.[0]?.[0];
|
|
150
153
|
|
|
151
154
|
if (typeof value === 'number') return value;
|