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.
Files changed (38) hide show
  1. package/README.md +715 -703
  2. package/dist/index.cjs +655 -75
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +170 -8
  5. package/dist/index.d.ts +170 -8
  6. package/dist/index.js +649 -75
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/scripts/generate-entities/render.mjs +24 -1
  10. package/scripts/naming-strategy.mjs +16 -1
  11. package/src/core/ast/procedure.ts +21 -0
  12. package/src/core/ast/query.ts +47 -19
  13. package/src/core/ddl/introspect/utils.ts +56 -56
  14. package/src/core/dialect/abstract.ts +560 -547
  15. package/src/core/dialect/base/sql-dialect.ts +43 -29
  16. package/src/core/dialect/mssql/index.ts +369 -232
  17. package/src/core/dialect/mysql/index.ts +99 -7
  18. package/src/core/dialect/postgres/index.ts +121 -60
  19. package/src/core/dialect/sqlite/index.ts +97 -64
  20. package/src/core/execution/db-executor.ts +108 -90
  21. package/src/core/execution/executors/mssql-executor.ts +28 -24
  22. package/src/core/execution/executors/mysql-executor.ts +62 -27
  23. package/src/core/execution/executors/sqlite-executor.ts +10 -9
  24. package/src/index.ts +9 -6
  25. package/src/orm/execute-procedure.ts +77 -0
  26. package/src/orm/execute.ts +74 -73
  27. package/src/orm/interceptor-pipeline.ts +21 -17
  28. package/src/orm/pooled-executor-factory.ts +41 -20
  29. package/src/orm/unit-of-work.ts +6 -4
  30. package/src/query/index.ts +8 -5
  31. package/src/query-builder/delete.ts +3 -2
  32. package/src/query-builder/insert-query-state.ts +47 -19
  33. package/src/query-builder/insert.ts +142 -28
  34. package/src/query-builder/procedure-call.ts +122 -0
  35. package/src/query-builder/select/select-operations.ts +5 -2
  36. package/src/query-builder/select.ts +1146 -1105
  37. package/src/query-builder/update.ts +3 -2
  38. 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 { ColumnNode } from '../core/ast/expression.js';
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 { InsertQueryNode, SelectQueryNode } from '../core/ast/query.js';
9
- import { InsertQueryState } from './insert-query-state.js';
10
- import { buildColumnNode } from '../core/ast/builders.js';
11
-
12
- type InsertDialectInput = Dialect | DialectKey;
13
-
14
- /**
15
- * Builder for INSERT queries
16
- */
17
- export class InsertQueryBuilder<T> {
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 results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
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 results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
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;