metal-orm 1.1.3 → 1.1.4

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 (37) 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/naming-strategy.mjs +16 -1
  10. package/src/core/ast/procedure.ts +21 -0
  11. package/src/core/ast/query.ts +47 -19
  12. package/src/core/ddl/introspect/utils.ts +56 -56
  13. package/src/core/dialect/abstract.ts +560 -547
  14. package/src/core/dialect/base/sql-dialect.ts +43 -29
  15. package/src/core/dialect/mssql/index.ts +369 -232
  16. package/src/core/dialect/mysql/index.ts +99 -7
  17. package/src/core/dialect/postgres/index.ts +121 -60
  18. package/src/core/dialect/sqlite/index.ts +97 -64
  19. package/src/core/execution/db-executor.ts +108 -90
  20. package/src/core/execution/executors/mssql-executor.ts +28 -24
  21. package/src/core/execution/executors/mysql-executor.ts +62 -27
  22. package/src/core/execution/executors/sqlite-executor.ts +10 -9
  23. package/src/index.ts +9 -6
  24. package/src/orm/execute-procedure.ts +77 -0
  25. package/src/orm/execute.ts +74 -73
  26. package/src/orm/interceptor-pipeline.ts +21 -17
  27. package/src/orm/pooled-executor-factory.ts +41 -20
  28. package/src/orm/unit-of-work.ts +6 -4
  29. package/src/query/index.ts +8 -5
  30. package/src/query-builder/delete.ts +3 -2
  31. package/src/query-builder/insert-query-state.ts +47 -19
  32. package/src/query-builder/insert.ts +142 -28
  33. package/src/query-builder/procedure-call.ts +122 -0
  34. package/src/query-builder/select/select-operations.ts +5 -2
  35. package/src/query-builder/select.ts +1146 -1105
  36. package/src/query-builder/update.ts +3 -2
  37. package/src/tree/tree-manager.ts +754 -754
@@ -1,6 +1,6 @@
1
- // src/core/execution/db-executor.ts
2
-
3
- // low-level canonical shape
1
+ // src/core/execution/db-executor.ts
2
+
3
+ // low-level canonical shape
4
4
  export type QueryResult = {
5
5
  columns: string[];
6
6
  values: unknown[][];
@@ -10,98 +10,116 @@ export type QueryResult = {
10
10
  };
11
11
  };
12
12
 
13
- export interface DbExecutor {
14
- /** Capability flags so the runtime can make correct decisions without relying on optional methods. */
15
- readonly capabilities: {
16
- /** True if begin/commit/rollback are real and should be used to provide atomicity. */
17
- transactions: boolean;
18
- };
19
-
20
- executeSql(sql: string, params?: unknown[]): Promise<QueryResult[]>;
21
-
22
- beginTransaction(): Promise<void>;
23
- commitTransaction(): Promise<void>;
24
- rollbackTransaction(): Promise<void>;
25
-
26
- /** Release any underlying resources (connections, pool leases, etc). Must be idempotent. */
27
- dispose(): Promise<void>;
28
- }
29
-
30
- // --- helpers ---
31
-
32
- /**
33
- * Convert an array of row objects into a QueryResult.
34
- */
35
- export function rowsToQueryResult(
36
- rows: Array<Record<string, unknown>>
37
- ): QueryResult {
38
- if (rows.length === 0) {
39
- return { columns: [], values: [] };
40
- }
41
-
42
- const columns = Object.keys(rows[0]);
43
- const values = rows.map(row => columns.map(c => row[c]));
44
- return { columns, values };
45
- }
46
-
47
13
  /**
48
- * Minimal contract that most SQL clients can implement.
14
+ * Canonical execution payload.
15
+ * It remains array-compatible for a gradual migration but always exposes
16
+ * `resultSets` for explicit multi-result handling.
49
17
  */
50
- export interface SimpleQueryRunner {
51
- query(
52
- sql: string,
53
- params?: unknown[]
54
- ): Promise<Array<Record<string, unknown>>>;
55
-
56
- /** Optional: used to support real transactions. */
57
- beginTransaction?(): Promise<void>;
58
- commitTransaction?(): Promise<void>;
59
- rollbackTransaction?(): Promise<void>;
60
-
61
- /** Optional: release resources (connection close, pool lease release, etc). */
62
- dispose?(): Promise<void>;
63
- }
18
+ export type ExecutionPayload = QueryResult[] & {
19
+ resultSets?: QueryResult[];
20
+ };
64
21
 
65
- /**
66
- * Generic factory: turn any SimpleQueryRunner into a DbExecutor.
67
- */
68
- export function createExecutorFromQueryRunner(
69
- runner: SimpleQueryRunner
70
- ): DbExecutor {
71
- const supportsTransactions =
72
- typeof runner.beginTransaction === 'function' &&
73
- typeof runner.commitTransaction === 'function' &&
74
- typeof runner.rollbackTransaction === 'function';
22
+ export const toExecutionPayload = (resultSets: QueryResult[]): ExecutionPayload => {
23
+ const payload = resultSets as ExecutionPayload;
24
+ payload.resultSets = resultSets;
25
+ return payload;
26
+ };
75
27
 
76
- return {
77
- capabilities: {
78
- transactions: supportsTransactions,
79
- },
28
+ export const payloadResultSets = (payload: ExecutionPayload): QueryResult[] =>
29
+ payload.resultSets ?? payload;
30
+
31
+ export interface DbExecutor {
32
+ /** Capability flags so the runtime can make correct decisions without relying on optional methods. */
33
+ readonly capabilities: {
34
+ /** True if begin/commit/rollback are real and should be used to provide atomicity. */
35
+ transactions: boolean;
36
+ };
37
+
38
+ executeSql(sql: string, params?: unknown[]): Promise<ExecutionPayload>;
39
+
40
+ beginTransaction(): Promise<void>;
41
+ commitTransaction(): Promise<void>;
42
+ rollbackTransaction(): Promise<void>;
43
+
44
+ /** Release any underlying resources (connections, pool leases, etc). Must be idempotent. */
45
+ dispose(): Promise<void>;
46
+ }
47
+
48
+ // --- helpers ---
49
+
50
+ /**
51
+ * Convert an array of row objects into a QueryResult.
52
+ */
53
+ export function rowsToQueryResult(
54
+ rows: Array<Record<string, unknown>>
55
+ ): QueryResult {
56
+ if (rows.length === 0) {
57
+ return { columns: [], values: [] };
58
+ }
59
+
60
+ const columns = Object.keys(rows[0]);
61
+ const values = rows.map(row => columns.map(c => row[c]));
62
+ return { columns, values };
63
+ }
64
+
65
+ /**
66
+ * Minimal contract that most SQL clients can implement.
67
+ */
68
+ export interface SimpleQueryRunner {
69
+ query(
70
+ sql: string,
71
+ params?: unknown[]
72
+ ): Promise<Array<Record<string, unknown>>>;
73
+
74
+ /** Optional: used to support real transactions. */
75
+ beginTransaction?(): Promise<void>;
76
+ commitTransaction?(): Promise<void>;
77
+ rollbackTransaction?(): Promise<void>;
78
+
79
+ /** Optional: release resources (connection close, pool lease release, etc). */
80
+ dispose?(): Promise<void>;
81
+ }
82
+
83
+ /**
84
+ * Generic factory: turn any SimpleQueryRunner into a DbExecutor.
85
+ */
86
+ export function createExecutorFromQueryRunner(
87
+ runner: SimpleQueryRunner
88
+ ): DbExecutor {
89
+ const supportsTransactions =
90
+ typeof runner.beginTransaction === 'function' &&
91
+ typeof runner.commitTransaction === 'function' &&
92
+ typeof runner.rollbackTransaction === 'function';
93
+
94
+ return {
95
+ capabilities: {
96
+ transactions: supportsTransactions,
97
+ },
80
98
  async executeSql(sql, params) {
81
99
  const rows = await runner.query(sql, params);
82
100
  const result = rowsToQueryResult(rows);
83
- return [result];
101
+ return toExecutionPayload([result]);
84
102
  },
85
- async beginTransaction() {
86
- if (!supportsTransactions) {
87
- throw new Error('Transactions are not supported by this executor');
88
- }
89
- await runner.beginTransaction!.call(runner);
90
- },
91
- async commitTransaction() {
92
- if (!supportsTransactions) {
93
- throw new Error('Transactions are not supported by this executor');
94
- }
95
- await runner.commitTransaction!.call(runner);
96
- },
97
- async rollbackTransaction() {
98
- if (!supportsTransactions) {
99
- throw new Error('Transactions are not supported by this executor');
100
- }
101
- await runner.rollbackTransaction!.call(runner);
102
- },
103
- async dispose() {
104
- await runner.dispose?.call(runner);
105
- },
106
- };
107
- }
103
+ async beginTransaction() {
104
+ if (!supportsTransactions) {
105
+ throw new Error('Transactions are not supported by this executor');
106
+ }
107
+ await runner.beginTransaction!.call(runner);
108
+ },
109
+ async commitTransaction() {
110
+ if (!supportsTransactions) {
111
+ throw new Error('Transactions are not supported by this executor');
112
+ }
113
+ await runner.commitTransaction!.call(runner);
114
+ },
115
+ async rollbackTransaction() {
116
+ if (!supportsTransactions) {
117
+ throw new Error('Transactions are not supported by this executor');
118
+ }
119
+ await runner.rollbackTransaction!.call(runner);
120
+ },
121
+ async dispose() {
122
+ await runner.dispose?.call(runner);
123
+ },
124
+ };
125
+ }
@@ -1,18 +1,22 @@
1
1
  // src/core/execution/executors/mssql-executor.ts
2
- import {
3
- DbExecutor,
4
- rowsToQueryResult
5
- } from '../db-executor.js';
6
-
7
- export interface MssqlClientLike {
8
- query(
9
- sql: string,
10
- params?: unknown[]
11
- ): Promise<{ recordset: Array<Record<string, unknown>> }>;
12
- beginTransaction?(): Promise<void>;
13
- commit?(): Promise<void>;
14
- rollback?(): Promise<void>;
15
- }
2
+ import {
3
+ DbExecutor,
4
+ toExecutionPayload,
5
+ rowsToQueryResult
6
+ } from '../db-executor.js';
7
+
8
+ export interface MssqlClientLike {
9
+ query(
10
+ sql: string,
11
+ params?: unknown[]
12
+ ): Promise<{
13
+ recordset?: Array<Record<string, unknown>>;
14
+ recordsets?: Array<Array<Record<string, unknown>>>;
15
+ }>;
16
+ beginTransaction?(): Promise<void>;
17
+ commit?(): Promise<void>;
18
+ rollback?(): Promise<void>;
19
+ }
16
20
 
17
21
  /**
18
22
  * Creates a database executor for Microsoft SQL Server.
@@ -28,14 +32,14 @@ export function createMssqlExecutor(
28
32
  typeof client.rollback === 'function';
29
33
 
30
34
  return {
31
- capabilities: {
32
- transactions: supportsTransactions,
33
- },
34
- async executeSql(sql, params) {
35
- const { recordset } = await client.query(sql, params);
36
- const result = rowsToQueryResult(recordset ?? []);
37
- return [result];
38
- },
35
+ capabilities: {
36
+ transactions: supportsTransactions,
37
+ },
38
+ async executeSql(sql, params) {
39
+ const { recordset, recordsets } = await client.query(sql, params);
40
+ const sets = Array.isArray(recordsets) ? recordsets : [recordset ?? []];
41
+ return toExecutionPayload(sets.map(set => rowsToQueryResult(set ?? [])));
42
+ },
39
43
  async beginTransaction() {
40
44
  if (!supportsTransactions) {
41
45
  throw new Error('Transactions are not supported by this executor');
@@ -157,8 +161,8 @@ export function createTediousMssqlClient(
157
161
  }
158
162
  );
159
163
 
160
- return { recordset: rows };
161
- },
164
+ return { recordset: rows, recordsets: [rows] };
165
+ },
162
166
 
163
167
  beginTransaction: connection.beginTransaction
164
168
  ? () =>
@@ -1,10 +1,12 @@
1
1
  // src/core/execution/executors/mysql-executor.ts
2
- import {
3
- DbExecutor,
4
- rowsToQueryResult
5
- } from '../db-executor.js';
2
+ import {
3
+ DbExecutor,
4
+ QueryResult,
5
+ toExecutionPayload,
6
+ rowsToQueryResult
7
+ } from '../db-executor.js';
6
8
 
7
- export interface MysqlClientLike {
9
+ export interface MysqlClientLike {
8
10
  query(
9
11
  sql: string,
10
12
  params?: unknown[]
@@ -12,7 +14,56 @@ export interface MysqlClientLike {
12
14
  beginTransaction?(): Promise<void>;
13
15
  commit?(): Promise<void>;
14
16
  rollback?(): Promise<void>;
15
- }
17
+ }
18
+
19
+ type RowObject = Record<string, unknown>;
20
+
21
+ const isRowObject = (value: unknown): value is RowObject =>
22
+ typeof value === 'object' && value !== null && !Array.isArray(value);
23
+
24
+ const isRowObjectArray = (value: unknown): value is RowObject[] =>
25
+ Array.isArray(value) && value.every(isRowObject);
26
+
27
+ const isMysqlResultHeader = (value: unknown): value is Record<string, unknown> =>
28
+ isRowObject(value) &&
29
+ ('affectedRows' in value ||
30
+ 'insertId' in value ||
31
+ 'warningStatus' in value ||
32
+ 'serverStatus' in value);
33
+
34
+ const headerToQueryResult = (header: Record<string, unknown>): QueryResult => ({
35
+ columns: [],
36
+ values: [],
37
+ meta: {
38
+ insertId: header.insertId as number | string | undefined,
39
+ rowsAffected: header.affectedRows as number | undefined,
40
+ }
41
+ });
42
+
43
+ const normalizeMysqlResults = (rows: unknown): QueryResult[] => {
44
+ if (!Array.isArray(rows)) {
45
+ return isMysqlResultHeader(rows)
46
+ ? [headerToQueryResult(rows)]
47
+ : [rowsToQueryResult([])];
48
+ }
49
+
50
+ if (isRowObjectArray(rows)) {
51
+ return [rowsToQueryResult(rows)];
52
+ }
53
+
54
+ const normalized: QueryResult[] = [];
55
+ for (const chunk of rows) {
56
+ if (isRowObjectArray(chunk)) {
57
+ normalized.push(rowsToQueryResult(chunk));
58
+ continue;
59
+ }
60
+ if (isMysqlResultHeader(chunk)) {
61
+ normalized.push(headerToQueryResult(chunk));
62
+ }
63
+ }
64
+
65
+ return normalized.length ? normalized : [rowsToQueryResult([])];
66
+ };
16
67
 
17
68
  /**
18
69
  * Creates a database executor for MySQL.
@@ -30,27 +81,11 @@ export function createMysqlExecutor(
30
81
  return {
31
82
  capabilities: {
32
83
  transactions: supportsTransactions,
33
- },
34
- async executeSql(sql, params) {
35
- const [rows] = await client.query(sql, params);
36
-
37
- if (!Array.isArray(rows)) {
38
- const header = rows as Record<string, unknown>;
39
- return [{
40
- columns: [],
41
- values: [],
42
- meta: {
43
- insertId: header.insertId as number | undefined,
44
- rowsAffected: header.affectedRows as number | undefined,
45
- }
46
- }];
47
- }
48
-
49
- const result = rowsToQueryResult(
50
- rows as Array<Record<string, unknown>>
51
- );
52
- return [result];
53
- },
84
+ },
85
+ async executeSql(sql, params) {
86
+ const [rows] = await client.query(sql, params);
87
+ return toExecutionPayload(normalizeMysqlResults(rows));
88
+ },
54
89
  async beginTransaction() {
55
90
  if (!supportsTransactions) {
56
91
  throw new Error('Transactions are not supported by this executor');
@@ -1,8 +1,9 @@
1
1
  // src/core/execution/executors/sqlite-executor.ts
2
- import {
3
- DbExecutor,
4
- rowsToQueryResult
5
- } from '../db-executor.js';
2
+ import {
3
+ DbExecutor,
4
+ toExecutionPayload,
5
+ rowsToQueryResult
6
+ } from '../db-executor.js';
6
7
 
7
8
  export interface SqliteClientLike {
8
9
  all(
@@ -32,11 +33,11 @@ export function createSqliteExecutor(
32
33
  capabilities: {
33
34
  transactions: supportsTransactions,
34
35
  },
35
- async executeSql(sql, params) {
36
- const rows = await client.all(sql, params);
37
- const result = rowsToQueryResult(rows);
38
- return [result];
39
- },
36
+ async executeSql(sql, params) {
37
+ const rows = await client.all(sql, params);
38
+ const result = rowsToQueryResult(rows);
39
+ return toExecutionPayload([result]);
40
+ },
40
41
  async beginTransaction() {
41
42
  if (!supportsTransactions) {
42
43
  throw new Error('Transactions are not supported by this executor');
package/src/index.ts CHANGED
@@ -9,11 +9,13 @@ export * from './schema/types.js';
9
9
  export * from './query-builder/select.js';
10
10
  export * from './query-builder/select-helpers.js';
11
11
  export * from './query-builder/insert.js';
12
- export * from './query-builder/update.js';
13
- export * from './query-builder/delete.js';
14
- export * from './query/index.js';
15
- export * from './core/ast/expression.js';
16
- export * from './core/ast/window-functions.js';
12
+ export * from './query-builder/update.js';
13
+ export * from './query-builder/delete.js';
14
+ export * from './query-builder/procedure-call.js';
15
+ export * from './query/index.js';
16
+ export * from './core/ast/expression.js';
17
+ export * from './core/ast/procedure.js';
18
+ export * from './core/ast/window-functions.js';
17
19
  export * from './core/hydration/types.js';
18
20
  export * from './core/dialect/mysql/index.js';
19
21
  export * from './core/dialect/mssql/index.js';
@@ -41,7 +43,8 @@ export * from './orm/lazy-batch.js';
41
43
  export * from './orm/relations/has-many.js';
42
44
  export * from './orm/relations/belongs-to.js';
43
45
  export * from './orm/relations/many-to-many.js';
44
- export * from './orm/execute.js';
46
+ export * from './orm/execute.js';
47
+ export * from './orm/execute-procedure.js';
45
48
  export type { EntityContext } from './orm/entity-context.js';
46
49
  export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
47
50
  export * from './orm/execution-context.js';
@@ -0,0 +1,77 @@
1
+ import type { ProcedureCallNode } from '../core/ast/procedure.js';
2
+ import type { QueryResult } from '../core/execution/db-executor.js';
3
+ import { payloadResultSets } from '../core/execution/db-executor.js';
4
+ import type { CompiledProcedureCall } from '../core/dialect/abstract.js';
5
+ import type { OrmSession } from './orm-session.js';
6
+
7
+ export interface ProcedureExecutionResult {
8
+ resultSets: QueryResult[];
9
+ out: Record<string, unknown>;
10
+ }
11
+
12
+ const resolveColumnIndex = (columns: string[], expectedName: string): number => {
13
+ const exact = columns.findIndex(column => column === expectedName);
14
+ if (exact >= 0) return exact;
15
+
16
+ const lowerExpected = expectedName.toLowerCase();
17
+ return columns.findIndex(column => column.toLowerCase() === lowerExpected);
18
+ };
19
+
20
+ const extractOutValues = (
21
+ compiled: CompiledProcedureCall,
22
+ resultSets: QueryResult[]
23
+ ): Record<string, unknown> => {
24
+ if (!compiled.outParams.names.length || compiled.outParams.source === 'none') {
25
+ return {};
26
+ }
27
+
28
+ const sourceSet =
29
+ compiled.outParams.source === 'firstResultSet'
30
+ ? resultSets[0]
31
+ : resultSets[resultSets.length - 1];
32
+
33
+ if (!sourceSet) {
34
+ throw new Error(
35
+ `Procedure expected OUT parameters in ${compiled.outParams.source}, but no result set was returned.`
36
+ );
37
+ }
38
+
39
+ if (!sourceSet.values.length) {
40
+ throw new Error(
41
+ `Procedure expected OUT parameters in ${compiled.outParams.source}, but the result set has no rows.`
42
+ );
43
+ }
44
+
45
+ const firstRow = sourceSet.values[0];
46
+ const out: Record<string, unknown> = {};
47
+ for (const expectedName of compiled.outParams.names) {
48
+ const columnIndex = resolveColumnIndex(sourceSet.columns, expectedName);
49
+ if (columnIndex < 0) {
50
+ const available = sourceSet.columns.length ? sourceSet.columns.join(', ') : '(none)';
51
+ throw new Error(
52
+ `Procedure OUT parameter "${expectedName}" was not found in ${compiled.outParams.source}. ` +
53
+ `Available columns: ${available}.`
54
+ );
55
+ }
56
+ out[expectedName] = firstRow[columnIndex];
57
+ }
58
+ return out;
59
+ };
60
+
61
+ export const executeProcedureAst = async (
62
+ session: OrmSession,
63
+ ast: ProcedureCallNode
64
+ ): Promise<ProcedureExecutionResult> => {
65
+ const execCtx = session.getExecutionContext();
66
+ const compiled = execCtx.dialect.compileProcedureCall(ast);
67
+ const payload = await execCtx.interceptors.run(
68
+ { sql: compiled.sql, params: compiled.params },
69
+ execCtx.executor
70
+ );
71
+ const resultSets = payloadResultSets(payload);
72
+
73
+ return {
74
+ resultSets,
75
+ out: extractOutValues(compiled, resultSets)
76
+ };
77
+ };