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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -40,14 +40,18 @@
|
|
|
40
40
|
"lint:fix": "node scripts/run-eslint.mjs --fix"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
+
"better-sqlite3": "^11.0.0",
|
|
44
|
+
"ioredis": "^5.0.0",
|
|
45
|
+
"keyv": "^5.6.0",
|
|
43
46
|
"mysql2": "^3.9.0",
|
|
44
47
|
"pg": "^8.0.0",
|
|
45
48
|
"sqlite3": "^5.1.6",
|
|
46
|
-
"tedious": "^19.0.0"
|
|
47
|
-
"keyv": "^5.6.0",
|
|
48
|
-
"ioredis": "^5.0.0"
|
|
49
|
+
"tedious": "^19.0.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
52
|
+
"better-sqlite3": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
51
55
|
"mysql2": {
|
|
52
56
|
"optional": true
|
|
53
57
|
},
|
|
@@ -68,24 +72,25 @@
|
|
|
68
72
|
}
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
71
|
-
"@electric-sql/pglite": "^0.
|
|
72
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
73
|
-
"@typescript-eslint/parser": "^8.
|
|
74
|
-
"@vitest/ui": "^4.
|
|
75
|
-
"
|
|
75
|
+
"@electric-sql/pglite": "^0.4.2",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
77
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
78
|
+
"@vitest/ui": "^4.1.2",
|
|
79
|
+
"better-sqlite3": "^12.8.0",
|
|
80
|
+
"eslint": "^10.1.0",
|
|
76
81
|
"express": "^5.2.1",
|
|
77
|
-
"ioredis": "^5.
|
|
78
|
-
"ioredis-mock": "^8.
|
|
82
|
+
"ioredis": "^5.10.1",
|
|
83
|
+
"ioredis-mock": "^8.13.1",
|
|
79
84
|
"keyv": "^5.6.0",
|
|
80
|
-
"mysql-memory-server": "^1.14.
|
|
81
|
-
"mysql2": "^3.
|
|
82
|
-
"pg": "^8.
|
|
83
|
-
"sqlite3": "^
|
|
85
|
+
"mysql-memory-server": "^1.14.1",
|
|
86
|
+
"mysql2": "^3.20.0",
|
|
87
|
+
"pg": "^8.20.0",
|
|
88
|
+
"sqlite3": "^6.0.1",
|
|
84
89
|
"supertest": "^7.2.2",
|
|
85
|
-
"tedious": "^19.2.
|
|
90
|
+
"tedious": "^19.2.1",
|
|
86
91
|
"tsup": "^8.5.1",
|
|
87
92
|
"tsx": "^4.21.0",
|
|
88
93
|
"typescript": "^5.9.3",
|
|
89
|
-
"vitest": "^4.
|
|
94
|
+
"vitest": "^4.1.2"
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
2
|
+
import type { ExecutionContext } from '../orm/execution-context.js';
|
|
3
|
+
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
4
|
+
import type { ColumnNode } from '../core/ast/expression.js';
|
|
5
|
+
import type { TableDef } from '../schema/table.js';
|
|
6
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
7
|
+
import type { QueryResult } from '../core/execution/db-executor.js';
|
|
8
|
+
|
|
9
|
+
export interface BulkExecutionContext {
|
|
10
|
+
readonly session: OrmSession;
|
|
11
|
+
readonly executionContext: ExecutionContext;
|
|
12
|
+
readonly dialect: Dialect;
|
|
13
|
+
readonly supportsReturning: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createBulkExecutionContext(session: OrmSession): BulkExecutionContext {
|
|
17
|
+
const executionContext = session.getExecutionContext();
|
|
18
|
+
return {
|
|
19
|
+
session,
|
|
20
|
+
executionContext,
|
|
21
|
+
dialect: executionContext.dialect,
|
|
22
|
+
supportsReturning: executionContext.dialect.supportsDmlReturningClause(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function executeCompiled(
|
|
27
|
+
ctx: BulkExecutionContext,
|
|
28
|
+
compiled: CompiledQuery
|
|
29
|
+
): Promise<QueryResult[]> {
|
|
30
|
+
const payload = await ctx.executionContext.interceptors.run(
|
|
31
|
+
{ sql: compiled.sql, params: compiled.params },
|
|
32
|
+
ctx.executionContext.executor
|
|
33
|
+
);
|
|
34
|
+
return extractResultSets(payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractResultSets(payload: unknown): QueryResult[] {
|
|
38
|
+
const result = payload as { resultSets?: QueryResult[] };
|
|
39
|
+
if (result.resultSets) {
|
|
40
|
+
return result.resultSets;
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function flattenQueryResults(resultSets: QueryResult[]): Record<string, unknown>[] {
|
|
46
|
+
const rows: Record<string, unknown>[] = [];
|
|
47
|
+
for (const rs of resultSets) {
|
|
48
|
+
for (const valueRow of rs.values) {
|
|
49
|
+
const obj: Record<string, unknown> = {};
|
|
50
|
+
rs.columns.forEach((col, idx) => {
|
|
51
|
+
const bare = col.split('.').pop()!.replace(/^["`[\]]+|["`[\]]+$/g, '');
|
|
52
|
+
obj[bare] = valueRow[idx];
|
|
53
|
+
});
|
|
54
|
+
rows.push(obj);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveReturningColumns(
|
|
61
|
+
ctx: BulkExecutionContext,
|
|
62
|
+
table: TableDef,
|
|
63
|
+
returning: boolean | ColumnDef[] | undefined
|
|
64
|
+
): ColumnNode[] | undefined {
|
|
65
|
+
if (!returning) return undefined;
|
|
66
|
+
if (!ctx.supportsReturning) return undefined;
|
|
67
|
+
|
|
68
|
+
if (returning === true) {
|
|
69
|
+
return Object.values(table.columns).map(col => ({
|
|
70
|
+
type: 'Column' as const,
|
|
71
|
+
table: table.name,
|
|
72
|
+
name: col.name,
|
|
73
|
+
alias: col.name,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return returning.map(col => ({
|
|
78
|
+
type: 'Column' as const,
|
|
79
|
+
table: table.name,
|
|
80
|
+
name: col.name,
|
|
81
|
+
alias: col.name,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
2
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
3
|
+
import { and, inList } from '../core/ast/expression-builders.js';
|
|
4
|
+
import type { TableDef } from '../schema/table.js';
|
|
5
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
6
|
+
import type { BulkDeleteOptions, ChunkOutcome } from './bulk-types.js';
|
|
7
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
8
|
+
import type { ExpressionNode } from '../core/ast/expression-nodes.js';
|
|
9
|
+
import { BulkBaseExecutor, type BulkExecutorOptions } from './bulk-executor.base.js';
|
|
10
|
+
import { createBulkExecutionContext, executeCompiled } from './bulk-context.js';
|
|
11
|
+
import { splitIntoChunks, runWithConcurrency, runChunk, maybeTransaction, aggregateOutcomes, aggregateOutcomesWithTimings } from './bulk-utils.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CHUNK_SIZE = 1000;
|
|
14
|
+
|
|
15
|
+
interface DeleteExecutorOptions extends BulkExecutorOptions {
|
|
16
|
+
by?: string;
|
|
17
|
+
where?: ExpressionNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BulkDeleteExecutor extends BulkBaseExecutor<DeleteExecutorOptions> {
|
|
21
|
+
private readonly byColumnName: string;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
session: OrmSession,
|
|
25
|
+
table: TableDef,
|
|
26
|
+
ids: ValueOperandInput[],
|
|
27
|
+
options: BulkDeleteOptions = {}
|
|
28
|
+
) {
|
|
29
|
+
super(session, table, ids, options);
|
|
30
|
+
this.byColumnName = options.by ?? findPrimaryKey(table);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected async executeChunk(chunk: ValueOperandInput[], chunkIndex: number): Promise<ChunkOutcome> {
|
|
34
|
+
const byColumn = this.table.columns[this.byColumnName];
|
|
35
|
+
if (!byColumn) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`bulkDelete: column "${this.byColumnName}" not found in table "${this.table.name}"`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const extraWhere = this.options.where;
|
|
42
|
+
const inExpr = inList(byColumn, chunk as any);
|
|
43
|
+
const finalWhere = extraWhere ? and(inExpr, extraWhere) : inExpr;
|
|
44
|
+
|
|
45
|
+
const builder = new DeleteQueryBuilder(this.table).where(finalWhere as ExpressionNode);
|
|
46
|
+
const compiled = builder.compile(this.ctx.dialect);
|
|
47
|
+
await executeCompiled(this.ctx, compiled);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
processedRows: chunk.length,
|
|
51
|
+
returning: [],
|
|
52
|
+
elapsedMs: 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function bulkDelete<TTable extends TableDef>(
|
|
58
|
+
session: OrmSession,
|
|
59
|
+
table: TTable,
|
|
60
|
+
ids: ValueOperandInput[],
|
|
61
|
+
options: BulkDeleteOptions = {}
|
|
62
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
63
|
+
if (!ids.length) {
|
|
64
|
+
return { processedRows: 0, chunksExecuted: 0, returning: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const executor = new BulkDeleteExecutor(session, table, ids, options);
|
|
68
|
+
return executor.execute();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function bulkDeleteWhere<TTable extends TableDef>(
|
|
72
|
+
session: OrmSession,
|
|
73
|
+
table: TTable,
|
|
74
|
+
where: ExpressionNode,
|
|
75
|
+
options: Pick<BulkDeleteOptions, 'transactional'> = {}
|
|
76
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
77
|
+
const { transactional = false } = options;
|
|
78
|
+
|
|
79
|
+
const ctx = createBulkExecutionContext(session);
|
|
80
|
+
const builder = new DeleteQueryBuilder(table).where(where);
|
|
81
|
+
const compiled = builder.compile(ctx.dialect);
|
|
82
|
+
|
|
83
|
+
const execute = async (): Promise<import('./bulk-types.js').BulkResult> => {
|
|
84
|
+
await executeCompiled(ctx, compiled);
|
|
85
|
+
return { processedRows: 0, chunksExecuted: 1, returning: [] };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return maybeTransaction(session, transactional, execute);
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
3
|
+
import type { BulkResult, BulkBaseOptions, ChunkOutcome } from './bulk-types.js';
|
|
4
|
+
import { createBulkExecutionContext, type BulkExecutionContext } from './bulk-context.js';
|
|
5
|
+
import {
|
|
6
|
+
splitIntoChunks,
|
|
7
|
+
runWithConcurrency,
|
|
8
|
+
runChunk,
|
|
9
|
+
maybeTransaction,
|
|
10
|
+
aggregateOutcomes,
|
|
11
|
+
aggregateOutcomesWithTimings,
|
|
12
|
+
} from './bulk-utils.js';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CHUNK_SIZE = 500;
|
|
15
|
+
|
|
16
|
+
export interface BulkExecutorOptions extends BulkBaseOptions {
|
|
17
|
+
chunkSize?: number;
|
|
18
|
+
concurrency?: 'sequential' | number;
|
|
19
|
+
transactional?: boolean;
|
|
20
|
+
timing?: boolean;
|
|
21
|
+
onChunkComplete?: (info: { chunkIndex: number; totalChunks: number; rowsInChunk: number; elapsedMs: number }) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export abstract class BulkBaseExecutor<TOptions extends BulkExecutorOptions> {
|
|
25
|
+
protected readonly session: OrmSession;
|
|
26
|
+
protected readonly table: TableDef;
|
|
27
|
+
protected readonly ctx: BulkExecutionContext;
|
|
28
|
+
protected readonly options: TOptions;
|
|
29
|
+
protected readonly chunks: unknown[][];
|
|
30
|
+
protected readonly totalChunks: number;
|
|
31
|
+
|
|
32
|
+
constructor(session: OrmSession, table: TableDef, rows: unknown[], options: TOptions = {} as TOptions) {
|
|
33
|
+
this.session = session;
|
|
34
|
+
this.table = table;
|
|
35
|
+
this.ctx = createBulkExecutionContext(session);
|
|
36
|
+
this.options = {
|
|
37
|
+
chunkSize: DEFAULT_CHUNK_SIZE,
|
|
38
|
+
concurrency: 'sequential',
|
|
39
|
+
transactional: true,
|
|
40
|
+
timing: false,
|
|
41
|
+
...options,
|
|
42
|
+
} as TOptions;
|
|
43
|
+
this.chunks = splitIntoChunks(rows, this.options.chunkSize!);
|
|
44
|
+
this.totalChunks = this.chunks.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected abstract executeChunk(chunk: unknown[], chunkIndex: number): Promise<ChunkOutcome>;
|
|
48
|
+
|
|
49
|
+
async execute(): Promise<BulkResult> {
|
|
50
|
+
const buildTask = (chunk: unknown[], chunkIndex: number) => async (): Promise<ChunkOutcome> => {
|
|
51
|
+
return runChunk(
|
|
52
|
+
() => this.executeChunk(chunk, chunkIndex),
|
|
53
|
+
chunkIndex,
|
|
54
|
+
this.totalChunks,
|
|
55
|
+
chunk.length,
|
|
56
|
+
this.options.timing!,
|
|
57
|
+
this.options.onChunkComplete
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const tasks = this.chunks.map((chunk, i) => buildTask(chunk, i));
|
|
62
|
+
|
|
63
|
+
const outcomes = await maybeTransaction(
|
|
64
|
+
this.session,
|
|
65
|
+
this.options.transactional!,
|
|
66
|
+
() => runWithConcurrency(tasks, this.options.concurrency!)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return this.options.timing
|
|
70
|
+
? aggregateOutcomesWithTimings(outcomes)
|
|
71
|
+
: aggregateOutcomes(outcomes);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { InsertQueryBuilder, ConflictBuilder } from '../query-builder/insert.js';
|
|
2
|
+
import type { TableDef } from '../schema/table.js';
|
|
3
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
4
|
+
import type { BulkInsertOptions, ChunkOutcome, InsertRow } from './bulk-types.js';
|
|
5
|
+
import { BulkBaseExecutor, type BulkExecutorOptions } from './bulk-executor.base.js';
|
|
6
|
+
import { resolveReturningColumns, flattenQueryResults, executeCompiled } from './bulk-context.js';
|
|
7
|
+
|
|
8
|
+
interface InsertExecutorOptions extends BulkExecutorOptions {
|
|
9
|
+
returning?: boolean | import('../schema/column-types.js').ColumnDef[];
|
|
10
|
+
onConflict?: import('../core/ast/query.js').UpsertClause;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BulkInsertExecutor extends BulkBaseExecutor<InsertExecutorOptions> {
|
|
14
|
+
constructor(
|
|
15
|
+
session: OrmSession,
|
|
16
|
+
table: TableDef,
|
|
17
|
+
rows: InsertRow[],
|
|
18
|
+
options: BulkInsertOptions = {}
|
|
19
|
+
) {
|
|
20
|
+
super(session, table, rows, options);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected async executeChunk(chunk: InsertRow[], chunkIndex: number): Promise<ChunkOutcome> {
|
|
24
|
+
const returningColumns = resolveReturningColumns(this.ctx, this.table, this.options.returning);
|
|
25
|
+
|
|
26
|
+
let builder: InsertQueryBuilder<unknown> | ConflictBuilder<unknown> =
|
|
27
|
+
new InsertQueryBuilder(this.table).values(chunk);
|
|
28
|
+
|
|
29
|
+
if (this.options.onConflict) {
|
|
30
|
+
const conflictColumns = this.options.onConflict.target?.columns ?? [];
|
|
31
|
+
builder = (builder as InsertQueryBuilder<unknown>).onConflict(conflictColumns as any);
|
|
32
|
+
|
|
33
|
+
if (this.options.onConflict.action.type === 'DoNothing') {
|
|
34
|
+
builder = (builder as ConflictBuilder<unknown>).doNothing();
|
|
35
|
+
} else if (this.options.onConflict.action.type === 'DoUpdate' && this.options.onConflict.action.set) {
|
|
36
|
+
const setMap: Record<string, unknown> = {};
|
|
37
|
+
for (const assignment of this.options.onConflict.action.set) {
|
|
38
|
+
const colName = typeof assignment.column === 'object' ? assignment.column.name : assignment.column;
|
|
39
|
+
setMap[colName] = assignment.value;
|
|
40
|
+
}
|
|
41
|
+
builder = (builder as ConflictBuilder<unknown>).doUpdate(setMap as any);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const finalBuilder = builder as InsertQueryBuilder<unknown>;
|
|
46
|
+
|
|
47
|
+
if (returningColumns?.length) {
|
|
48
|
+
finalBuilder.returning(...returningColumns);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const compiled = finalBuilder.compile(this.ctx.dialect);
|
|
52
|
+
const resultSets = await executeCompiled(this.ctx, compiled);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
processedRows: chunk.length,
|
|
56
|
+
returning: returningColumns ? flattenQueryResults(resultSets) : [],
|
|
57
|
+
elapsedMs: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function bulkInsert<TTable extends TableDef>(
|
|
63
|
+
session: OrmSession,
|
|
64
|
+
table: TTable,
|
|
65
|
+
rows: InsertRow[],
|
|
66
|
+
options: BulkInsertOptions = {}
|
|
67
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
68
|
+
if (!rows.length) {
|
|
69
|
+
return { processedRows: 0, chunksExecuted: 0, returning: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const executor = new BulkInsertExecutor(session, table, rows, options);
|
|
73
|
+
return executor.execute();
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
3
|
+
import type { ExpressionNode } from '../core/ast/expression-nodes.js';
|
|
4
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
5
|
+
import type { UpsertClause } from '../core/ast/query.js';
|
|
6
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
7
|
+
|
|
8
|
+
export type { TableDef, OrmSession };
|
|
9
|
+
|
|
10
|
+
export type BulkConcurrency = 'sequential' | number;
|
|
11
|
+
|
|
12
|
+
export interface BulkResult {
|
|
13
|
+
processedRows: number;
|
|
14
|
+
chunksExecuted: number;
|
|
15
|
+
returning: Record<string, unknown>[];
|
|
16
|
+
chunkTimings?: number[];
|
|
17
|
+
metadata?: BulkResultMetadata;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BulkResultMetadata {
|
|
21
|
+
strategy: 'individual' | 'batch' | 'whereIn';
|
|
22
|
+
dialect: string;
|
|
23
|
+
hasReturningSupport: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BulkBaseOptions {
|
|
27
|
+
chunkSize?: number;
|
|
28
|
+
concurrency?: BulkConcurrency;
|
|
29
|
+
transactional?: boolean;
|
|
30
|
+
timing?: boolean;
|
|
31
|
+
onChunkComplete?: (info: ChunkCompleteInfo) => void | Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChunkCompleteInfo {
|
|
35
|
+
chunkIndex: number;
|
|
36
|
+
totalChunks: number;
|
|
37
|
+
rowsInChunk: number;
|
|
38
|
+
elapsedMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type InsertRow = Record<string, ValueOperandInput>;
|
|
42
|
+
|
|
43
|
+
export interface BulkInsertOptions extends BulkBaseOptions {
|
|
44
|
+
returning?: boolean | ColumnDef[];
|
|
45
|
+
onConflict?: UpsertClause;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BulkUpsertOptions extends BulkInsertOptions {
|
|
49
|
+
conflictColumns?: string[];
|
|
50
|
+
updateColumns?: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type UpdateRow = Record<string, ValueOperandInput>;
|
|
54
|
+
|
|
55
|
+
export interface BulkUpdateOptions extends BulkBaseOptions {
|
|
56
|
+
by?: string | string[];
|
|
57
|
+
where?: ExpressionNode;
|
|
58
|
+
returning?: boolean | ColumnDef[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BulkDeleteOptions extends BulkBaseOptions {
|
|
62
|
+
by?: string;
|
|
63
|
+
where?: ExpressionNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ChunkOutcome {
|
|
67
|
+
processedRows: number;
|
|
68
|
+
returning: Record<string, unknown>[];
|
|
69
|
+
elapsedMs: number;
|
|
70
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
2
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
3
|
+
import { eq, and, inList } from '../core/ast/expression-builders.js';
|
|
4
|
+
import type { TableDef } from '../schema/table.js';
|
|
5
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
6
|
+
import type { BulkUpdateOptions, ChunkOutcome, UpdateRow } from './bulk-types.js';
|
|
7
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
8
|
+
import type { ExpressionNode } from '../core/ast/expression-nodes.js';
|
|
9
|
+
import { BulkBaseExecutor, type BulkExecutorOptions } from './bulk-executor.base.js';
|
|
10
|
+
import { resolveReturningColumns, flattenQueryResults, executeCompiled, createBulkExecutionContext } from './bulk-context.js';
|
|
11
|
+
import { splitIntoChunks, runWithConcurrency, runChunk, maybeTransaction, aggregateOutcomes, aggregateOutcomesWithTimings } from './bulk-utils.js';
|
|
12
|
+
|
|
13
|
+
interface UpdateExecutorOptions extends BulkExecutorOptions {
|
|
14
|
+
by?: string | string[];
|
|
15
|
+
where?: ExpressionNode;
|
|
16
|
+
returning?: boolean | import('../schema/column-types.js').ColumnDef[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveByColumns(table: TableDef, by: string | string[] | undefined): string[] {
|
|
20
|
+
if (!by) return [findPrimaryKey(table)];
|
|
21
|
+
return Array.isArray(by) ? by : [by];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BulkUpdateExecutor extends BulkBaseExecutor<UpdateExecutorOptions> {
|
|
25
|
+
private readonly byColumns: string[];
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
session: OrmSession,
|
|
29
|
+
table: TableDef,
|
|
30
|
+
rows: UpdateRow[],
|
|
31
|
+
options: BulkUpdateOptions = {}
|
|
32
|
+
) {
|
|
33
|
+
super(session, table, rows, options);
|
|
34
|
+
this.byColumns = resolveByColumns(table, options.by);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected async executeChunk(chunk: UpdateRow[], chunkIndex: number): Promise<ChunkOutcome> {
|
|
38
|
+
const allReturning: Record<string, unknown>[] = [];
|
|
39
|
+
const returningColumns = resolveReturningColumns(this.ctx, this.table, this.options.returning);
|
|
40
|
+
const extraWhere = this.options.where;
|
|
41
|
+
|
|
42
|
+
for (const row of chunk) {
|
|
43
|
+
const predicates = this.byColumns.map(colName => {
|
|
44
|
+
const col = this.table.columns[colName];
|
|
45
|
+
if (!col) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`bulkUpdate: column "${colName}" not found in table "${this.table.name}"`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const val = row[colName];
|
|
51
|
+
if (val === undefined) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`bulkUpdate: row is missing the identity column "${colName}" required by the "by" option`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return eq(col, val as ValueOperandInput);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const whereExpr: ExpressionNode =
|
|
60
|
+
predicates.length === 1
|
|
61
|
+
? predicates[0]
|
|
62
|
+
: predicates.reduce((acc, p) => and(acc, p) as ExpressionNode, predicates[0]);
|
|
63
|
+
|
|
64
|
+
const finalWhere = extraWhere ? and(whereExpr, extraWhere) as ExpressionNode : whereExpr;
|
|
65
|
+
|
|
66
|
+
const bySet = new Set(this.byColumns);
|
|
67
|
+
const setPayload: Record<string, unknown> = {};
|
|
68
|
+
for (const [key, val] of Object.entries(row)) {
|
|
69
|
+
if (!bySet.has(key) && key in this.table.columns) {
|
|
70
|
+
setPayload[key] = val;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Object.keys(setPayload).length) continue;
|
|
75
|
+
|
|
76
|
+
let builder = new UpdateQueryBuilder(this.table).set(setPayload).where(finalWhere);
|
|
77
|
+
|
|
78
|
+
if (returningColumns?.length) {
|
|
79
|
+
builder = builder.returning(...returningColumns);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const compiled = builder.compile(this.ctx.dialect);
|
|
83
|
+
const resultSets = await executeCompiled(this.ctx, compiled);
|
|
84
|
+
|
|
85
|
+
if (returningColumns) {
|
|
86
|
+
allReturning.push(...flattenQueryResults(resultSets));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
processedRows: chunk.length,
|
|
92
|
+
returning: allReturning,
|
|
93
|
+
elapsedMs: 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function bulkUpdate<TTable extends TableDef>(
|
|
99
|
+
session: OrmSession,
|
|
100
|
+
table: TTable,
|
|
101
|
+
rows: UpdateRow[],
|
|
102
|
+
options: BulkUpdateOptions = {}
|
|
103
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
104
|
+
if (!rows.length) {
|
|
105
|
+
return { processedRows: 0, chunksExecuted: 0, returning: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const executor = new BulkUpdateExecutor(session, table, rows, options);
|
|
109
|
+
return executor.execute();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const DEFAULT_BULK_UPDATE_WHERE_CHUNK_SIZE = 500;
|
|
113
|
+
|
|
114
|
+
export async function bulkUpdateWhere<TTable extends TableDef>(
|
|
115
|
+
session: OrmSession,
|
|
116
|
+
table: TTable,
|
|
117
|
+
ids: ValueOperandInput[],
|
|
118
|
+
set: Record<string, ValueOperandInput>,
|
|
119
|
+
options: Omit<BulkUpdateOptions, 'by' | 'returning'> & {
|
|
120
|
+
by?: string;
|
|
121
|
+
returning?: boolean | import('../schema/column-types.js').ColumnDef[];
|
|
122
|
+
} = {}
|
|
123
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
124
|
+
if (!ids.length) {
|
|
125
|
+
return { processedRows: 0, chunksExecuted: 0, returning: [] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
chunkSize = DEFAULT_BULK_UPDATE_WHERE_CHUNK_SIZE,
|
|
130
|
+
concurrency = 'sequential',
|
|
131
|
+
transactional = true,
|
|
132
|
+
timing = false,
|
|
133
|
+
onChunkComplete,
|
|
134
|
+
by,
|
|
135
|
+
where: extraWhere,
|
|
136
|
+
returning,
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
const ctx = createBulkExecutionContext(session);
|
|
140
|
+
const byColumnName = by ?? findPrimaryKey(table);
|
|
141
|
+
const byColumn = table.columns[byColumnName];
|
|
142
|
+
if (!byColumn) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`bulkUpdateWhere: column "${byColumnName}" not found in table "${table.name}"`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const returningColumns = resolveReturningColumns(ctx, table, returning);
|
|
149
|
+
const chunks = splitIntoChunks(ids, chunkSize);
|
|
150
|
+
const totalChunks = chunks.length;
|
|
151
|
+
|
|
152
|
+
const buildTask = (chunk: ValueOperandInput[], chunkIndex: number) => async (): Promise<ChunkOutcome> => {
|
|
153
|
+
return runChunk(
|
|
154
|
+
async () => {
|
|
155
|
+
const inExpr = inList(byColumn, chunk as any);
|
|
156
|
+
const finalWhere = extraWhere ? and(inExpr, extraWhere) : inExpr;
|
|
157
|
+
|
|
158
|
+
let builder = new UpdateQueryBuilder(table).set(set as Record<string, unknown>).where(finalWhere);
|
|
159
|
+
|
|
160
|
+
if (returningColumns?.length) {
|
|
161
|
+
builder = builder.returning(...returningColumns);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const compiled = builder.compile(ctx.dialect);
|
|
165
|
+
const resultSets = await executeCompiled(ctx, compiled);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
processedRows: chunk.length,
|
|
169
|
+
returning: returningColumns ? flattenQueryResults(resultSets) : [],
|
|
170
|
+
elapsedMs: 0,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
chunkIndex,
|
|
174
|
+
totalChunks,
|
|
175
|
+
chunk.length,
|
|
176
|
+
timing,
|
|
177
|
+
onChunkComplete
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const tasks = chunks.map((chunk, i) => buildTask(chunk, i));
|
|
182
|
+
|
|
183
|
+
const outcomes = await maybeTransaction(
|
|
184
|
+
session,
|
|
185
|
+
transactional,
|
|
186
|
+
() => runWithConcurrency(tasks, concurrency)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return timing
|
|
190
|
+
? aggregateOutcomesWithTimings(outcomes)
|
|
191
|
+
: aggregateOutcomes(outcomes);
|
|
192
|
+
}
|