metal-orm 1.1.8 → 1.1.10
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 +769 -764
- package/dist/index.cjs +2352 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +605 -40
- package/dist/index.d.ts +605 -40
- package/dist/index.js +2324 -226
- package/dist/index.js.map +1 -1
- package/package.json +22 -17
- package/src/bulk/bulk-context.ts +83 -0
- package/src/bulk/bulk-delete-executor.ts +89 -0
- package/src/bulk/bulk-executor.base.ts +73 -0
- package/src/bulk/bulk-insert-executor.ts +74 -0
- package/src/bulk/bulk-types.ts +70 -0
- package/src/bulk/bulk-update-executor.ts +192 -0
- package/src/bulk/bulk-upsert-executor.ts +95 -0
- package/src/bulk/bulk-utils.ts +91 -0
- package/src/bulk/index.ts +18 -0
- package/src/codegen/typescript.ts +30 -21
- package/src/core/ast/expression-builders.ts +107 -10
- package/src/core/ast/expression-nodes.ts +52 -22
- package/src/core/ast/expression-visitor.ts +23 -13
- package/src/core/dialect/abstract.ts +30 -17
- package/src/core/dialect/mysql/index.ts +20 -5
- package/src/core/execution/db-executor.ts +96 -64
- package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
- package/src/core/execution/executors/mssql-executor.ts +66 -34
- package/src/core/execution/executors/mysql-executor.ts +98 -66
- package/src/core/execution/executors/postgres-executor.ts +33 -11
- package/src/core/execution/executors/sqlite-executor.ts +86 -30
- package/src/decorators/bootstrap.ts +482 -398
- package/src/decorators/column-decorator.ts +87 -96
- package/src/decorators/decorator-metadata.ts +100 -24
- package/src/decorators/entity.ts +27 -24
- package/src/decorators/relations.ts +231 -149
- package/src/decorators/transformers/transformer-decorators.ts +26 -29
- package/src/decorators/validators/country-validators-decorators.ts +9 -15
- package/src/dto/apply-filter.ts +568 -551
- package/src/index.ts +16 -9
- package/src/orm/entity-hydration.ts +116 -72
- package/src/orm/entity-metadata.ts +347 -301
- package/src/orm/entity-relations.ts +264 -207
- package/src/orm/entity.ts +199 -199
- package/src/orm/execute.ts +13 -13
- package/src/orm/lazy-batch/morph-many.ts +70 -0
- package/src/orm/lazy-batch/morph-one.ts +69 -0
- package/src/orm/lazy-batch/morph-to.ts +59 -0
- package/src/orm/lazy-batch.ts +4 -1
- package/src/orm/orm-session.ts +170 -104
- package/src/orm/pooled-executor-factory.ts +99 -58
- package/src/orm/query-logger.ts +49 -40
- package/src/orm/relation-change-processor.ts +198 -96
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -0
- package/src/orm/relations/morph-one.ts +151 -0
- package/src/orm/relations/morph-to.ts +162 -0
- package/src/orm/save-graph.ts +116 -1
- package/src/query-builder/expression-table-mapper.ts +5 -0
- package/src/query-builder/hydration-manager.ts +345 -345
- package/src/query-builder/hydration-planner.ts +178 -148
- package/src/query-builder/relation-conditions.ts +171 -151
- package/src/query-builder/relation-cte-builder.ts +5 -1
- package/src/query-builder/relation-filter-utils.ts +9 -6
- package/src/query-builder/relation-include-strategies.ts +44 -2
- package/src/query-builder/relation-join-strategies.ts +8 -1
- package/src/query-builder/relation-service.ts +250 -241
- package/src/query-builder/select/cursor-pagination.ts +323 -0
- package/src/query-builder/select/select-operations.ts +110 -105
- package/src/query-builder/select.ts +42 -1
- package/src/query-builder/update-include.ts +4 -0
- package/src/schema/relation.ts +296 -188
- package/src/schema/types.ts +138 -123
- package/src/tree/tree-decorator.ts +127 -137
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
// src/core/execution/db-executor.ts
|
|
2
2
|
|
|
3
3
|
// low-level canonical shape
|
|
4
|
-
export type QueryResult = {
|
|
5
|
-
columns: string[];
|
|
6
|
-
values: unknown[][];
|
|
7
|
-
meta?: {
|
|
8
|
-
insertId?: number | string;
|
|
9
|
-
rowsAffected?: number;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Canonical execution payload.
|
|
15
|
-
* It remains array-compatible for a gradual migration but always exposes
|
|
16
|
-
* `resultSets` for explicit multi-result handling.
|
|
17
|
-
*/
|
|
18
|
-
export type ExecutionPayload = QueryResult[] & {
|
|
19
|
-
resultSets?: QueryResult[];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const toExecutionPayload = (resultSets: QueryResult[]): ExecutionPayload => {
|
|
23
|
-
const payload = resultSets as ExecutionPayload;
|
|
24
|
-
payload.resultSets = resultSets;
|
|
25
|
-
return payload;
|
|
26
|
-
};
|
|
27
|
-
|
|
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;
|
|
4
|
+
export type QueryResult = {
|
|
5
|
+
columns: string[];
|
|
6
|
+
values: unknown[][];
|
|
7
|
+
meta?: {
|
|
8
|
+
insertId?: number | string;
|
|
9
|
+
rowsAffected?: number;
|
|
36
10
|
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canonical execution payload.
|
|
15
|
+
* It remains array-compatible for a gradual migration but always exposes
|
|
16
|
+
* `resultSets` for explicit multi-result handling.
|
|
17
|
+
*/
|
|
18
|
+
export type ExecutionPayload = QueryResult[] & {
|
|
19
|
+
resultSets?: QueryResult[];
|
|
20
|
+
};
|
|
37
21
|
|
|
38
|
-
|
|
22
|
+
export const toExecutionPayload = (resultSets: QueryResult[]): ExecutionPayload => {
|
|
23
|
+
const payload = resultSets as ExecutionPayload;
|
|
24
|
+
payload.resultSets = resultSets;
|
|
25
|
+
return payload;
|
|
26
|
+
};
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
/** True if savepoint/release/rollback-to-savepoint are implemented. */
|
|
37
|
+
savepoints?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
executeSql(sql: string, params?: unknown[]): Promise<ExecutionPayload>;
|
|
41
|
+
|
|
42
|
+
beginTransaction(): Promise<void>;
|
|
43
|
+
commitTransaction(): Promise<void>;
|
|
44
|
+
rollbackTransaction(): Promise<void>;
|
|
45
|
+
savepoint?(name: string): Promise<void>;
|
|
46
|
+
releaseSavepoint?(name: string): Promise<void>;
|
|
47
|
+
rollbackToSavepoint?(name: string): Promise<void>;
|
|
43
48
|
|
|
44
49
|
/** Release any underlying resources (connections, pool leases, etc). Must be idempotent. */
|
|
45
50
|
dispose(): Promise<void>;
|
|
@@ -71,10 +76,13 @@ export interface SimpleQueryRunner {
|
|
|
71
76
|
params?: unknown[]
|
|
72
77
|
): Promise<Array<Record<string, unknown>>>;
|
|
73
78
|
|
|
74
|
-
/** Optional: used to support real transactions. */
|
|
75
|
-
beginTransaction?(): Promise<void>;
|
|
76
|
-
commitTransaction?(): Promise<void>;
|
|
77
|
-
rollbackTransaction?(): Promise<void>;
|
|
79
|
+
/** Optional: used to support real transactions. */
|
|
80
|
+
beginTransaction?(): Promise<void>;
|
|
81
|
+
commitTransaction?(): Promise<void>;
|
|
82
|
+
rollbackTransaction?(): Promise<void>;
|
|
83
|
+
savepoint?(name: string): Promise<void>;
|
|
84
|
+
releaseSavepoint?(name: string): Promise<void>;
|
|
85
|
+
rollbackToSavepoint?(name: string): Promise<void>;
|
|
78
86
|
|
|
79
87
|
/** Optional: release resources (connection close, pool lease release, etc). */
|
|
80
88
|
dispose?(): Promise<void>;
|
|
@@ -86,20 +94,26 @@ export interface SimpleQueryRunner {
|
|
|
86
94
|
export function createExecutorFromQueryRunner(
|
|
87
95
|
runner: SimpleQueryRunner
|
|
88
96
|
): DbExecutor {
|
|
89
|
-
const supportsTransactions =
|
|
90
|
-
typeof runner.beginTransaction === 'function' &&
|
|
91
|
-
typeof runner.commitTransaction === 'function' &&
|
|
92
|
-
typeof runner.rollbackTransaction === 'function';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const supportsTransactions =
|
|
98
|
+
typeof runner.beginTransaction === 'function' &&
|
|
99
|
+
typeof runner.commitTransaction === 'function' &&
|
|
100
|
+
typeof runner.rollbackTransaction === 'function';
|
|
101
|
+
const supportsSavepoints =
|
|
102
|
+
supportsTransactions &&
|
|
103
|
+
typeof runner.savepoint === 'function' &&
|
|
104
|
+
typeof runner.releaseSavepoint === 'function' &&
|
|
105
|
+
typeof runner.rollbackToSavepoint === 'function';
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
capabilities: {
|
|
109
|
+
transactions: supportsTransactions,
|
|
110
|
+
...(supportsSavepoints ? { savepoints: true } : {}),
|
|
102
111
|
},
|
|
112
|
+
async executeSql(sql, params) {
|
|
113
|
+
const rows = await runner.query(sql, params);
|
|
114
|
+
const result = rowsToQueryResult(rows);
|
|
115
|
+
return toExecutionPayload([result]);
|
|
116
|
+
},
|
|
103
117
|
async beginTransaction() {
|
|
104
118
|
if (!supportsTransactions) {
|
|
105
119
|
throw new Error('Transactions are not supported by this executor');
|
|
@@ -112,14 +126,32 @@ export function createExecutorFromQueryRunner(
|
|
|
112
126
|
}
|
|
113
127
|
await runner.commitTransaction!.call(runner);
|
|
114
128
|
},
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
async rollbackTransaction() {
|
|
130
|
+
if (!supportsTransactions) {
|
|
131
|
+
throw new Error('Transactions are not supported by this executor');
|
|
132
|
+
}
|
|
133
|
+
await runner.rollbackTransaction!.call(runner);
|
|
134
|
+
},
|
|
135
|
+
async savepoint(name: string) {
|
|
136
|
+
if (!supportsSavepoints) {
|
|
137
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
138
|
+
}
|
|
139
|
+
await runner.savepoint!.call(runner, name);
|
|
140
|
+
},
|
|
141
|
+
async releaseSavepoint(name: string) {
|
|
142
|
+
if (!supportsSavepoints) {
|
|
143
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
144
|
+
}
|
|
145
|
+
await runner.releaseSavepoint!.call(runner, name);
|
|
146
|
+
},
|
|
147
|
+
async rollbackToSavepoint(name: string) {
|
|
148
|
+
if (!supportsSavepoints) {
|
|
149
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
150
|
+
}
|
|
151
|
+
await runner.rollbackToSavepoint!.call(runner, name);
|
|
152
|
+
},
|
|
153
|
+
async dispose() {
|
|
154
|
+
await runner.dispose?.call(runner);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/core/execution/executors/better-sqlite3-executor.ts
|
|
2
|
+
import {
|
|
3
|
+
DbExecutor,
|
|
4
|
+
toExecutionPayload,
|
|
5
|
+
rowsToQueryResult,
|
|
6
|
+
QueryResult
|
|
7
|
+
} from '../db-executor.js';
|
|
8
|
+
|
|
9
|
+
export interface BetterSqlite3Statement {
|
|
10
|
+
reader: boolean;
|
|
11
|
+
all(...params: unknown[]): unknown[];
|
|
12
|
+
run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BetterSqlite3ClientLike {
|
|
16
|
+
prepare(sql: string): BetterSqlite3Statement;
|
|
17
|
+
transaction<T extends (...args: any[]) => any>(fn: T): T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SAVEPOINT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
21
|
+
|
|
22
|
+
const sanitizeSavepointName = (name: string): string => {
|
|
23
|
+
const trimmed = name.trim();
|
|
24
|
+
if (!SAVEPOINT_NAME_PATTERN.test(trimmed)) {
|
|
25
|
+
throw new Error(`Invalid savepoint name: "${name}"`);
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a database executor for better-sqlite3.
|
|
32
|
+
* @param client A better-sqlite3 database instance.
|
|
33
|
+
* @returns A DbExecutor implementation for better-sqlite3.
|
|
34
|
+
*/
|
|
35
|
+
export function createBetterSqlite3Executor(
|
|
36
|
+
client: BetterSqlite3ClientLike
|
|
37
|
+
): DbExecutor {
|
|
38
|
+
// better-sqlite3 handles nested transactions using savepoints automatically
|
|
39
|
+
// when using .transaction(), but DbExecutor needs explicit control.
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
capabilities: {
|
|
43
|
+
transactions: true,
|
|
44
|
+
savepoints: true,
|
|
45
|
+
},
|
|
46
|
+
async executeSql(sql, params) {
|
|
47
|
+
const stmt = client.prepare(sql);
|
|
48
|
+
let result: QueryResult;
|
|
49
|
+
|
|
50
|
+
if (stmt.reader) {
|
|
51
|
+
const rows = stmt.all(...(params ?? [])) as Record<string, unknown>[];
|
|
52
|
+
result = rowsToQueryResult(rows);
|
|
53
|
+
} else {
|
|
54
|
+
const info = stmt.run(...(params ?? []));
|
|
55
|
+
result = {
|
|
56
|
+
columns: [],
|
|
57
|
+
values: [],
|
|
58
|
+
meta: {
|
|
59
|
+
rowsAffected: info.changes,
|
|
60
|
+
insertId: typeof info.lastInsertRowid === 'bigint'
|
|
61
|
+
? info.lastInsertRowid.toString()
|
|
62
|
+
: info.lastInsertRowid
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return toExecutionPayload([result]);
|
|
68
|
+
},
|
|
69
|
+
async beginTransaction() {
|
|
70
|
+
client.prepare('BEGIN').run();
|
|
71
|
+
},
|
|
72
|
+
async commitTransaction() {
|
|
73
|
+
client.prepare('COMMIT').run();
|
|
74
|
+
},
|
|
75
|
+
async rollbackTransaction() {
|
|
76
|
+
client.prepare('ROLLBACK').run();
|
|
77
|
+
},
|
|
78
|
+
async savepoint(name: string) {
|
|
79
|
+
const savepoint = sanitizeSavepointName(name);
|
|
80
|
+
client.prepare(`SAVEPOINT ${savepoint}`).run();
|
|
81
|
+
},
|
|
82
|
+
async releaseSavepoint(name: string) {
|
|
83
|
+
const savepoint = sanitizeSavepointName(name);
|
|
84
|
+
client.prepare(`RELEASE SAVEPOINT ${savepoint}`).run();
|
|
85
|
+
},
|
|
86
|
+
async rollbackToSavepoint(name: string) {
|
|
87
|
+
const savepoint = sanitizeSavepointName(name);
|
|
88
|
+
client.prepare(`ROLLBACK TO SAVEPOINT ${savepoint}`).run();
|
|
89
|
+
},
|
|
90
|
+
async dispose() {
|
|
91
|
+
// Connection lifecycle is owned by the caller.
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
// src/core/execution/executors/mssql-executor.ts
|
|
2
|
-
import {
|
|
3
|
-
DbExecutor,
|
|
4
|
-
toExecutionPayload,
|
|
5
|
-
rowsToQueryResult
|
|
6
|
-
} from '../db-executor.js';
|
|
2
|
+
import {
|
|
3
|
+
DbExecutor,
|
|
4
|
+
toExecutionPayload,
|
|
5
|
+
rowsToQueryResult
|
|
6
|
+
} from '../db-executor.js';
|
|
7
7
|
|
|
8
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>;
|
|
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
18
|
rollback?(): Promise<void>;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
const SAVEPOINT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
|
+
|
|
23
|
+
const sanitizeSavepointName = (name: string): string => {
|
|
24
|
+
const trimmed = name.trim();
|
|
25
|
+
if (!SAVEPOINT_NAME_PATTERN.test(trimmed)) {
|
|
26
|
+
throw new Error(`Invalid savepoint name: "${name}"`);
|
|
27
|
+
}
|
|
28
|
+
return trimmed;
|
|
29
|
+
};
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Creates a database executor for Microsoft SQL Server.
|
|
@@ -26,20 +36,22 @@ export interface MssqlClientLike {
|
|
|
26
36
|
export function createMssqlExecutor(
|
|
27
37
|
client: MssqlClientLike
|
|
28
38
|
): DbExecutor {
|
|
29
|
-
const supportsTransactions =
|
|
30
|
-
typeof client.beginTransaction === 'function' &&
|
|
31
|
-
typeof client.commit === 'function' &&
|
|
32
|
-
typeof client.rollback === 'function';
|
|
39
|
+
const supportsTransactions =
|
|
40
|
+
typeof client.beginTransaction === 'function' &&
|
|
41
|
+
typeof client.commit === 'function' &&
|
|
42
|
+
typeof client.rollback === 'function';
|
|
43
|
+
const supportsSavepoints = supportsTransactions;
|
|
33
44
|
|
|
34
45
|
return {
|
|
35
46
|
capabilities: {
|
|
36
47
|
transactions: supportsTransactions,
|
|
48
|
+
...(supportsSavepoints ? { savepoints: true } : {}),
|
|
37
49
|
},
|
|
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
|
-
},
|
|
50
|
+
async executeSql(sql, params) {
|
|
51
|
+
const { recordset, recordsets } = await client.query(sql, params);
|
|
52
|
+
const sets = Array.isArray(recordsets) ? recordsets : [recordset ?? []];
|
|
53
|
+
return toExecutionPayload(sets.map(set => rowsToQueryResult(set ?? [])));
|
|
54
|
+
},
|
|
43
55
|
async beginTransaction() {
|
|
44
56
|
if (!supportsTransactions) {
|
|
45
57
|
throw new Error('Transactions are not supported by this executor');
|
|
@@ -52,15 +64,35 @@ export function createMssqlExecutor(
|
|
|
52
64
|
}
|
|
53
65
|
await client.commit!();
|
|
54
66
|
},
|
|
55
|
-
async rollbackTransaction() {
|
|
56
|
-
if (!supportsTransactions) {
|
|
57
|
-
throw new Error('Transactions are not supported by this executor');
|
|
58
|
-
}
|
|
59
|
-
await client.rollback!();
|
|
60
|
-
},
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
async rollbackTransaction() {
|
|
68
|
+
if (!supportsTransactions) {
|
|
69
|
+
throw new Error('Transactions are not supported by this executor');
|
|
70
|
+
}
|
|
71
|
+
await client.rollback!();
|
|
72
|
+
},
|
|
73
|
+
async savepoint(name: string) {
|
|
74
|
+
if (!supportsSavepoints) {
|
|
75
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
76
|
+
}
|
|
77
|
+
const savepoint = sanitizeSavepointName(name);
|
|
78
|
+
await client.query(`SAVE TRANSACTION ${savepoint}`);
|
|
79
|
+
},
|
|
80
|
+
async releaseSavepoint(_name: string) {
|
|
81
|
+
if (!supportsSavepoints) {
|
|
82
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
83
|
+
}
|
|
84
|
+
// SQL Server does not expose a RELEASE SAVEPOINT statement.
|
|
85
|
+
},
|
|
86
|
+
async rollbackToSavepoint(name: string) {
|
|
87
|
+
if (!supportsSavepoints) {
|
|
88
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
89
|
+
}
|
|
90
|
+
const savepoint = sanitizeSavepointName(name);
|
|
91
|
+
await client.query(`ROLLBACK TRANSACTION ${savepoint}`);
|
|
92
|
+
},
|
|
93
|
+
async dispose() {
|
|
94
|
+
// Connection lifecycle is owned by the caller/driver. Pool lease executors should implement dispose.
|
|
95
|
+
},
|
|
64
96
|
};
|
|
65
97
|
}
|
|
66
98
|
|
|
@@ -161,8 +193,8 @@ export function createTediousMssqlClient(
|
|
|
161
193
|
}
|
|
162
194
|
);
|
|
163
195
|
|
|
164
|
-
return { recordset: rows, recordsets: [rows] };
|
|
165
|
-
},
|
|
196
|
+
return { recordset: rows, recordsets: [rows] };
|
|
197
|
+
},
|
|
166
198
|
|
|
167
199
|
beginTransaction: connection.beginTransaction
|
|
168
200
|
? () =>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/core/execution/executors/mysql-executor.ts
|
|
2
|
-
import {
|
|
3
|
-
DbExecutor,
|
|
4
|
-
QueryResult,
|
|
5
|
-
toExecutionPayload,
|
|
6
|
-
rowsToQueryResult
|
|
7
|
-
} from '../db-executor.js';
|
|
2
|
+
import {
|
|
3
|
+
DbExecutor,
|
|
4
|
+
QueryResult,
|
|
5
|
+
toExecutionPayload,
|
|
6
|
+
rowsToQueryResult
|
|
7
|
+
} from '../db-executor.js';
|
|
8
8
|
|
|
9
|
-
export interface MysqlClientLike {
|
|
9
|
+
export interface MysqlClientLike {
|
|
10
10
|
query(
|
|
11
11
|
sql: string,
|
|
12
12
|
params?: unknown[]
|
|
@@ -14,57 +14,66 @@ export interface MysqlClientLike {
|
|
|
14
14
|
beginTransaction?(): Promise<void>;
|
|
15
15
|
commit?(): Promise<void>;
|
|
16
16
|
rollback?(): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
|
|
17
|
+
}
|
|
18
|
+
|
|
19
19
|
type RowObject = Record<string, unknown>;
|
|
20
|
+
const SAVEPOINT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
20
21
|
|
|
21
22
|
const isRowObject = (value: unknown): value is RowObject =>
|
|
22
23
|
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
|
-
|
|
24
|
+
|
|
25
|
+
const isRowObjectArray = (value: unknown): value is RowObject[] =>
|
|
26
|
+
Array.isArray(value) && value.every(isRowObject);
|
|
27
|
+
|
|
28
|
+
const isMysqlResultHeader = (value: unknown): value is Record<string, unknown> =>
|
|
29
|
+
isRowObject(value) &&
|
|
30
|
+
('affectedRows' in value ||
|
|
31
|
+
'insertId' in value ||
|
|
32
|
+
'warningStatus' in value ||
|
|
33
|
+
'serverStatus' in value);
|
|
34
|
+
|
|
34
35
|
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,
|
|
36
|
+
columns: [],
|
|
37
|
+
values: [],
|
|
38
|
+
meta: {
|
|
39
|
+
insertId: header.insertId as number | string | undefined,
|
|
40
|
+
rowsAffected: header.affectedRows as number | undefined,
|
|
40
41
|
}
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: [rowsToQueryResult([])];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (isRowObjectArray(rows)) {
|
|
51
|
-
return [rowsToQueryResult(rows)];
|
|
44
|
+
const sanitizeSavepointName = (name: string): string => {
|
|
45
|
+
const trimmed = name.trim();
|
|
46
|
+
if (!SAVEPOINT_NAME_PATTERN.test(trimmed)) {
|
|
47
|
+
throw new Error(`Invalid savepoint name: "${name}"`);
|
|
52
48
|
}
|
|
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([])];
|
|
49
|
+
return trimmed;
|
|
66
50
|
};
|
|
67
51
|
|
|
52
|
+
const normalizeMysqlResults = (rows: unknown): QueryResult[] => {
|
|
53
|
+
if (!Array.isArray(rows)) {
|
|
54
|
+
return isMysqlResultHeader(rows)
|
|
55
|
+
? [headerToQueryResult(rows)]
|
|
56
|
+
: [rowsToQueryResult([])];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isRowObjectArray(rows)) {
|
|
60
|
+
return [rowsToQueryResult(rows)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalized: QueryResult[] = [];
|
|
64
|
+
for (const chunk of rows) {
|
|
65
|
+
if (isRowObjectArray(chunk)) {
|
|
66
|
+
normalized.push(rowsToQueryResult(chunk));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (isMysqlResultHeader(chunk)) {
|
|
70
|
+
normalized.push(headerToQueryResult(chunk));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return normalized.length ? normalized : [rowsToQueryResult([])];
|
|
75
|
+
};
|
|
76
|
+
|
|
68
77
|
/**
|
|
69
78
|
* Creates a database executor for MySQL.
|
|
70
79
|
* @param client A MySQL client instance.
|
|
@@ -73,19 +82,21 @@ const normalizeMysqlResults = (rows: unknown): QueryResult[] => {
|
|
|
73
82
|
export function createMysqlExecutor(
|
|
74
83
|
client: MysqlClientLike
|
|
75
84
|
): DbExecutor {
|
|
76
|
-
const supportsTransactions =
|
|
77
|
-
typeof client.beginTransaction === 'function' &&
|
|
78
|
-
typeof client.commit === 'function' &&
|
|
79
|
-
typeof client.rollback === 'function';
|
|
85
|
+
const supportsTransactions =
|
|
86
|
+
typeof client.beginTransaction === 'function' &&
|
|
87
|
+
typeof client.commit === 'function' &&
|
|
88
|
+
typeof client.rollback === 'function';
|
|
89
|
+
const supportsSavepoints = supportsTransactions;
|
|
80
90
|
|
|
81
91
|
return {
|
|
82
|
-
capabilities: {
|
|
83
|
-
transactions: supportsTransactions,
|
|
84
|
-
|
|
85
|
-
async executeSql(sql, params) {
|
|
86
|
-
const [rows] = await client.query(sql, params);
|
|
87
|
-
return toExecutionPayload(normalizeMysqlResults(rows));
|
|
92
|
+
capabilities: {
|
|
93
|
+
transactions: supportsTransactions,
|
|
94
|
+
...(supportsSavepoints ? { savepoints: true } : {}),
|
|
88
95
|
},
|
|
96
|
+
async executeSql(sql, params) {
|
|
97
|
+
const [rows] = await client.query(sql, params);
|
|
98
|
+
return toExecutionPayload(normalizeMysqlResults(rows));
|
|
99
|
+
},
|
|
89
100
|
async beginTransaction() {
|
|
90
101
|
if (!supportsTransactions) {
|
|
91
102
|
throw new Error('Transactions are not supported by this executor');
|
|
@@ -98,14 +109,35 @@ export function createMysqlExecutor(
|
|
|
98
109
|
}
|
|
99
110
|
await client.commit!();
|
|
100
111
|
},
|
|
101
|
-
async rollbackTransaction() {
|
|
102
|
-
if (!supportsTransactions) {
|
|
103
|
-
throw new Error('Transactions are not supported by this executor');
|
|
104
|
-
}
|
|
105
|
-
await client.rollback!();
|
|
106
|
-
},
|
|
107
|
-
async
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
async rollbackTransaction() {
|
|
113
|
+
if (!supportsTransactions) {
|
|
114
|
+
throw new Error('Transactions are not supported by this executor');
|
|
115
|
+
}
|
|
116
|
+
await client.rollback!();
|
|
117
|
+
},
|
|
118
|
+
async savepoint(name: string) {
|
|
119
|
+
if (!supportsSavepoints) {
|
|
120
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
121
|
+
}
|
|
122
|
+
const savepoint = sanitizeSavepointName(name);
|
|
123
|
+
await client.query(`SAVEPOINT ${savepoint}`);
|
|
124
|
+
},
|
|
125
|
+
async releaseSavepoint(name: string) {
|
|
126
|
+
if (!supportsSavepoints) {
|
|
127
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
128
|
+
}
|
|
129
|
+
const savepoint = sanitizeSavepointName(name);
|
|
130
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
131
|
+
},
|
|
132
|
+
async rollbackToSavepoint(name: string) {
|
|
133
|
+
if (!supportsSavepoints) {
|
|
134
|
+
throw new Error('Savepoints are not supported by this executor');
|
|
135
|
+
}
|
|
136
|
+
const savepoint = sanitizeSavepointName(name);
|
|
137
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
138
|
+
},
|
|
139
|
+
async dispose() {
|
|
140
|
+
// Connection lifecycle is owned by the caller/driver. Pool lease executors should implement dispose.
|
|
141
|
+
},
|
|
110
142
|
};
|
|
111
143
|
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
// src/core/execution/executors/postgres-executor.ts
|
|
2
|
-
import {
|
|
3
|
-
DbExecutor,
|
|
4
|
-
createExecutorFromQueryRunner
|
|
5
|
-
} from '../db-executor.js';
|
|
2
|
+
import {
|
|
3
|
+
DbExecutor,
|
|
4
|
+
createExecutorFromQueryRunner
|
|
5
|
+
} from '../db-executor.js';
|
|
6
6
|
|
|
7
|
-
export interface PostgresClientLike {
|
|
7
|
+
export interface PostgresClientLike {
|
|
8
8
|
query(
|
|
9
9
|
text: string,
|
|
10
10
|
params?: unknown[]
|
|
11
11
|
): Promise<{ rows: Array<Record<string, unknown>> }>;
|
|
12
|
-
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SAVEPOINT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
|
+
|
|
16
|
+
const sanitizeSavepointName = (name: string): string => {
|
|
17
|
+
const trimmed = name.trim();
|
|
18
|
+
if (!SAVEPOINT_NAME_PATTERN.test(trimmed)) {
|
|
19
|
+
throw new Error(`Invalid savepoint name: "${name}"`);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
};
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* Creates a database executor for PostgreSQL.
|
|
@@ -30,8 +40,20 @@ export function createPostgresExecutor(
|
|
|
30
40
|
async commitTransaction() {
|
|
31
41
|
await client.query('COMMIT');
|
|
32
42
|
},
|
|
33
|
-
async rollbackTransaction() {
|
|
34
|
-
await client.query('ROLLBACK');
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
async rollbackTransaction() {
|
|
44
|
+
await client.query('ROLLBACK');
|
|
45
|
+
},
|
|
46
|
+
async savepoint(name: string) {
|
|
47
|
+
const savepoint = sanitizeSavepointName(name);
|
|
48
|
+
await client.query(`SAVEPOINT ${savepoint}`);
|
|
49
|
+
},
|
|
50
|
+
async releaseSavepoint(name: string) {
|
|
51
|
+
const savepoint = sanitizeSavepointName(name);
|
|
52
|
+
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
53
|
+
},
|
|
54
|
+
async rollbackToSavepoint(name: string) {
|
|
55
|
+
const savepoint = sanitizeSavepointName(name);
|
|
56
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|