linkgress-orm 0.4.55 → 0.4.57
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/dist/entity/db-context.d.ts +233 -4
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +709 -96
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/query/conditions.d.ts +41 -0
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +48 -0
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/mutation-batch.d.ts +190 -0
- package/dist/query/mutation-batch.d.ts.map +1 -0
- package/dist/query/mutation-batch.js +316 -0
- package/dist/query/mutation-batch.js.map +1 -0
- package/dist/query/query-batch.d.ts +6 -0
- package/dist/query/query-batch.d.ts.map +1 -1
- package/dist/query/query-batch.js +8 -9
- package/dist/query/query-batch.js.map +1 -1
- package/dist/query/query-builder.d.ts +6 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +40 -8
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/sql-utils.d.ts +1 -0
- package/dist/query/sql-utils.d.ts.map +1 -1
- package/dist/query/sql-utils.js +12 -1
- package/dist/query/sql-utils.js.map +1 -1
- package/package.json +80 -79
|
@@ -7,10 +7,11 @@ const entity_base_1 = require("./entity-base");
|
|
|
7
7
|
const model_config_1 = require("./model-config");
|
|
8
8
|
const conditions_1 = require("../query/conditions");
|
|
9
9
|
const query_builder_1 = require("../query/query-builder");
|
|
10
|
+
const sql_utils_1 = require("../query/sql-utils");
|
|
10
11
|
const db_schema_manager_1 = require("../migration/db-schema-manager");
|
|
11
12
|
const sequence_builder_1 = require("../schema/sequence-builder");
|
|
12
13
|
const cte_root_query_1 = require("../query/cte-root-query");
|
|
13
|
-
const
|
|
14
|
+
const sql_utils_2 = require("../query/sql-utils");
|
|
14
15
|
/**
|
|
15
16
|
* Per-schema cache of the entity column mapping plan used by
|
|
16
17
|
* DbEntityTable.mapResultToEntity. Schema objects come from the shared
|
|
@@ -539,9 +540,9 @@ class InsertBuilder {
|
|
|
539
540
|
}
|
|
540
541
|
valuePlaceholders.push(`(${rowPlaceholders.join(', ')})`);
|
|
541
542
|
}
|
|
542
|
-
const columnNames = (0,
|
|
543
|
-
const returningColumns = (0,
|
|
544
|
-
const qualifiedTableName = (0,
|
|
543
|
+
const columnNames = (0, sql_utils_2.buildColumnNamesList)(this.schema, columns);
|
|
544
|
+
const returningColumns = (0, sql_utils_2.buildReturningColumnList)(this.schema);
|
|
545
|
+
const qualifiedTableName = (0, sql_utils_2.getQualifiedTableName)(this.schema);
|
|
545
546
|
let sql = `INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})`;
|
|
546
547
|
// Add OVERRIDING SYSTEM VALUE if specified
|
|
547
548
|
if (this.overridingSystemValue) {
|
|
@@ -753,8 +754,8 @@ class TableAccessor {
|
|
|
753
754
|
placeholders.push(`$${paramIndex++}`);
|
|
754
755
|
}
|
|
755
756
|
}
|
|
756
|
-
const returningColumns = (0,
|
|
757
|
-
const qualifiedTableName = (0,
|
|
757
|
+
const returningColumns = (0, sql_utils_2.buildReturningColumnList)(this.schema);
|
|
758
|
+
const qualifiedTableName = (0, sql_utils_2.getQualifiedTableName)(this.schema);
|
|
758
759
|
const sql = `
|
|
759
760
|
INSERT INTO ${qualifiedTableName} (${columns.join(', ')})
|
|
760
761
|
VALUES (${placeholders.join(', ')})
|
|
@@ -775,7 +776,7 @@ class TableAccessor {
|
|
|
775
776
|
}
|
|
776
777
|
// Calculate chunk size based on max rows per batch
|
|
777
778
|
const columnCount = Object.keys(dataArray[0]).length;
|
|
778
|
-
const chunkSize = (0,
|
|
779
|
+
const chunkSize = (0, sql_utils_2.calculateOptimalChunkSize)(columnCount, insertConfig?.chunkSize);
|
|
779
780
|
// Check if we need to chunk
|
|
780
781
|
if (dataArray.length > chunkSize) {
|
|
781
782
|
const results = [];
|
|
@@ -798,15 +799,15 @@ class TableAccessor {
|
|
|
798
799
|
return [];
|
|
799
800
|
}
|
|
800
801
|
// Extract all unique column names from all data objects
|
|
801
|
-
const columns = (0,
|
|
802
|
+
const columns = (0, sql_utils_2.extractUniqueColumnKeys)(dataArray, this.schema, insertConfig?.overridingSystemValue);
|
|
802
803
|
if (columns.length === 0) {
|
|
803
804
|
return [];
|
|
804
805
|
}
|
|
805
|
-
const columnConfigs = (0,
|
|
806
|
-
const { valueClauses, params } = (0,
|
|
806
|
+
const columnConfigs = (0, sql_utils_2.buildColumnConfigs)(this.schema, columns, insertConfig?.overridingSystemValue);
|
|
807
|
+
const { valueClauses, params } = (0, sql_utils_2.buildValuesClause)(dataArray, columnConfigs);
|
|
807
808
|
const columnNames = columnConfigs.map(c => `"${c.dbName}"`);
|
|
808
|
-
const returningColumns = (0,
|
|
809
|
-
const qualifiedTableName = (0,
|
|
809
|
+
const returningColumns = (0, sql_utils_2.buildReturningColumnList)(this.schema);
|
|
810
|
+
const qualifiedTableName = (0, sql_utils_2.getQualifiedTableName)(this.schema);
|
|
810
811
|
let sql = `
|
|
811
812
|
INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})`;
|
|
812
813
|
// Add OVERRIDING SYSTEM VALUE if specified
|
|
@@ -838,10 +839,10 @@ class TableAccessor {
|
|
|
838
839
|
// Determine primary keys
|
|
839
840
|
const primaryKeys = config?.primaryKey
|
|
840
841
|
? (Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey])
|
|
841
|
-
: (0,
|
|
842
|
+
: (0, sql_utils_2.detectPrimaryKeys)(this.schema);
|
|
842
843
|
// Auto-detect overridingSystemValue
|
|
843
844
|
const overridingSystemValue = config?.overridingSystemValue ??
|
|
844
|
-
(0,
|
|
845
|
+
(0, sql_utils_2.hasAutoIncrementPrimaryKey)(this.schema, Object.keys(referenceItem));
|
|
845
846
|
// Determine which columns to update
|
|
846
847
|
let updateColumnFilter = config?.updateColumnFilter;
|
|
847
848
|
if (updateColumnFilter == null && config?.updateColumns) {
|
|
@@ -853,7 +854,7 @@ class TableAccessor {
|
|
|
853
854
|
}
|
|
854
855
|
// Calculate chunk size based on max rows per batch
|
|
855
856
|
const columnCount = Object.keys(values[0]).length;
|
|
856
|
-
const chunkSize = (0,
|
|
857
|
+
const chunkSize = (0, sql_utils_2.calculateOptimalChunkSize)(columnCount, config?.chunkSize);
|
|
857
858
|
// Check if we need to chunk
|
|
858
859
|
if (values.length > chunkSize) {
|
|
859
860
|
const results = [];
|
|
@@ -935,7 +936,7 @@ class TableAccessor {
|
|
|
935
936
|
const returningColumns = Object.entries(this.schema.columns)
|
|
936
937
|
.map(([_, col]) => `"${col.build().name}"`)
|
|
937
938
|
.join(', ');
|
|
938
|
-
const qualifiedTableName = (0,
|
|
939
|
+
const qualifiedTableName = (0, sql_utils_2.getQualifiedTableName)(this.schema);
|
|
939
940
|
const sql = `
|
|
940
941
|
UPDATE ${qualifiedTableName}
|
|
941
942
|
SET ${setClauses.join(', ')}
|
|
@@ -963,7 +964,7 @@ class TableAccessor {
|
|
|
963
964
|
if (!pkColumnName) {
|
|
964
965
|
throw new Error(`Table ${this.schema.name} has no primary key`);
|
|
965
966
|
}
|
|
966
|
-
const qualifiedTableName = (0,
|
|
967
|
+
const qualifiedTableName = (0, sql_utils_2.getQualifiedTableName)(this.schema);
|
|
967
968
|
const sql = `DELETE FROM ${qualifiedTableName} WHERE "${pkColumnName}" = $1`;
|
|
968
969
|
const result = this.executor
|
|
969
970
|
? await this.executor.query(sql, [id])
|
|
@@ -2022,14 +2023,475 @@ class DbEntityTable {
|
|
|
2022
2023
|
}
|
|
2023
2024
|
};
|
|
2024
2025
|
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Insert ONE parent row and its dependent CHILD rows as a SINGLE statement,
|
|
2028
|
+
* optionally guarded by a NOT-EXISTS probe:
|
|
2029
|
+
*
|
|
2030
|
+
* WITH "__iwc_parent__" AS (
|
|
2031
|
+
* INSERT INTO parent (cols) SELECT ... FROM (VALUES (...)) v
|
|
2032
|
+
* [WHERE NOT EXISTS (SELECT 1 FROM (<unlessExists>) "__iwc_guard__")]
|
|
2033
|
+
* RETURNING <parent cols>
|
|
2034
|
+
* ),
|
|
2035
|
+
* "__mutation__" AS (
|
|
2036
|
+
* INSERT INTO child (fk, cols)
|
|
2037
|
+
* SELECT p.pk, v.cols FROM "__iwc_parent__" p CROSS JOIN (VALUES (0, ...), (1, ...)) v("__iwc_ord", cols)
|
|
2038
|
+
* ORDER BY v."__iwc_ord"
|
|
2039
|
+
* RETURNING <child cols>
|
|
2040
|
+
* )
|
|
2041
|
+
* SELECT <child projection>, <parent cols> FROM "__mutation__" CROSS JOIN "__iwc_parent__" ...
|
|
2042
|
+
*
|
|
2043
|
+
* Guarantees:
|
|
2044
|
+
* - the child rows receive the freshly inserted parent's primary key via
|
|
2045
|
+
* `children.foreignKey`;
|
|
2046
|
+
* - the ordinal ORDER BY fixes the child sequence-allocation order, so
|
|
2047
|
+
* serial child ids ascend in INPUT-ROW order, and the returned `children`
|
|
2048
|
+
* array is sorted back into that order (deterministic even when the
|
|
2049
|
+
* outer navigation joins would otherwise shuffle rows);
|
|
2050
|
+
* - a matching `unlessExists` guard suppresses the WHOLE insert in the same
|
|
2051
|
+
* snapshot and resolves `{ parent: null, children: [] }` (callers race-
|
|
2052
|
+
* guarding concurrent creates still need their own serialization — two
|
|
2053
|
+
* concurrent statements cannot see each other's uncommitted rows);
|
|
2054
|
+
* - single-statement atomicity: any failing leg rolls back both inserts.
|
|
2055
|
+
*
|
|
2056
|
+
* Restrictions: single-column auto/serial parent primary key; the parent
|
|
2057
|
+
* `returning` selector supports FLAT parent columns only; child rows must
|
|
2058
|
+
* NOT carry the foreign-key property; `children.rows` must be non-empty and
|
|
2059
|
+
* fit one statement (no chunking). The child `returning` selector supports
|
|
2060
|
+
* the full navigation/collection projection surface of `.returning()`.
|
|
2061
|
+
*/
|
|
2062
|
+
insertWithChildren(config) {
|
|
2063
|
+
const { rows, foreignKey } = config.children;
|
|
2064
|
+
if (rows.length === 0) {
|
|
2065
|
+
throw new Error('insertWithChildren: children.rows must be non-empty — use a plain insert for a childless parent');
|
|
2066
|
+
}
|
|
2067
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2068
|
+
if (rows[i][foreignKey] !== undefined) {
|
|
2069
|
+
throw new Error(`insertWithChildren: child row at index ${i} carries the foreign-key property "${foreignKey}" — it is sourced from the inserted parent`);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const columnCount = Math.max(1, Object.keys(rows[0]).length + 1);
|
|
2073
|
+
const singleStatementLimit = Math.floor(Math.floor(65535 / columnCount) * 0.6);
|
|
2074
|
+
if (rows.length > singleStatementLimit) {
|
|
2075
|
+
throw new Error(`insertWithChildren: ${rows.length} child rows exceed the ~${singleStatementLimit}-row single-statement budget — insert them standalone (they need chunking)`);
|
|
2076
|
+
}
|
|
2077
|
+
return this.executeInsertWithChildren(config);
|
|
2078
|
+
}
|
|
2079
|
+
/** @internal Async body of {@link insertWithChildren} (validation stays synchronous). */
|
|
2080
|
+
async executeInsertWithChildren(config) {
|
|
2081
|
+
const parentSchema = this._getSchema();
|
|
2082
|
+
const executor = this._getExecutor();
|
|
2083
|
+
const client = this._getClient();
|
|
2084
|
+
const childTable = config.children.table;
|
|
2085
|
+
if (childTable._getClient() !== client || childTable._getExecutor() !== executor) {
|
|
2086
|
+
throw new Error('insertWithChildren: the child table uses a different database client or transaction than the parent — both must share one connection context');
|
|
2087
|
+
}
|
|
2088
|
+
if (config.unlessExists) {
|
|
2089
|
+
const guardFuture = config.unlessExists.future();
|
|
2090
|
+
if (guardFuture._client !== client || guardFuture._executor !== executor) {
|
|
2091
|
+
throw new Error('insertWithChildren: the unlessExists guard uses a different database client or transaction than the insert');
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
// Single-column parent primary key — the value the child FK column selects.
|
|
2095
|
+
const pkEntries = Object.entries(parentSchema.columns).filter(([, colBuilder]) => colBuilder.build().primaryKey);
|
|
2096
|
+
if (pkEntries.length !== 1) {
|
|
2097
|
+
throw new Error('insertWithChildren requires a single-column parent primary key');
|
|
2098
|
+
}
|
|
2099
|
+
const parentPkDbName = pkEntries[0][1].build().name;
|
|
2100
|
+
// ---- parent leg: INSERT ... SELECT FROM (VALUES ...) [WHERE NOT EXISTS] ----
|
|
2101
|
+
const parentCompiled = this.compileValuesWithCasts(parentSchema, [config.row], null);
|
|
2102
|
+
const parentColNames = parentCompiled.columns.map(c => `"${c.dbName}"`).join(', ');
|
|
2103
|
+
const parentSelectCols = parentCompiled.columns.map(c => `v."${c.dbName}"`).join(', ');
|
|
2104
|
+
const params = [...parentCompiled.params];
|
|
2105
|
+
// Parent RETURNING: the selector's flat columns plus the pk (child FK source).
|
|
2106
|
+
const parentMock = this.createMockEntity();
|
|
2107
|
+
const parentSelection = config.returning.parent(parentMock);
|
|
2108
|
+
const parentSelCols = [];
|
|
2109
|
+
for (const [prop, field] of Object.entries(parentSelection)) {
|
|
2110
|
+
const tableAlias = field?.__tableAlias;
|
|
2111
|
+
const dbColumnName = field?.__dbColumnName;
|
|
2112
|
+
if (dbColumnName == null || (tableAlias && tableAlias !== parentSchema.name)) {
|
|
2113
|
+
throw new Error(`insertWithChildren: parent returning supports flat parent columns only — "${prop}" is not one`);
|
|
2114
|
+
}
|
|
2115
|
+
const colEntry = Object.entries(parentSchema.columns).find(([, colBuilder]) => colBuilder.build().name === dbColumnName);
|
|
2116
|
+
parentSelCols.push({ prop, dbName: dbColumnName, mapper: colEntry ? colEntry[1].build().mapper : undefined });
|
|
2117
|
+
}
|
|
2118
|
+
let parentSql = `INSERT INTO ${this._getQualifiedTableName()} (${parentColNames})
|
|
2119
|
+
SELECT ${parentSelectCols} FROM (VALUES (${parentCompiled.valueRows[0]})) AS v(${parentColNames})`;
|
|
2120
|
+
if (config.unlessExists) {
|
|
2121
|
+
const guardFuture = config.unlessExists.future();
|
|
2122
|
+
const guardSql = params.length === 0 ? guardFuture._sql : (0, sql_utils_1.renumberPlaceholders)(guardFuture._sql, params.length);
|
|
2123
|
+
parentSql += `\nWHERE NOT EXISTS (SELECT 1 FROM (\n${guardSql}\n) "__iwc_guard__")`;
|
|
2124
|
+
params.push(...guardFuture._params);
|
|
2125
|
+
}
|
|
2126
|
+
// RETURNING * so the CTE can stand in for the parent TABLE in child→parent
|
|
2127
|
+
// navigation joins (the real table is snapshot-stale within this statement).
|
|
2128
|
+
parentSql += '\nRETURNING *';
|
|
2129
|
+
// ---- child leg: INSERT ... SELECT parent-pk + ordered VALUES ----
|
|
2130
|
+
const childSchema = childTable._getSchema();
|
|
2131
|
+
const fkColBuilder = childSchema.columns[config.children.foreignKey];
|
|
2132
|
+
if (!fkColBuilder) {
|
|
2133
|
+
throw new Error(`insertWithChildren: unknown child foreign-key property "${config.children.foreignKey}"`);
|
|
2134
|
+
}
|
|
2135
|
+
const fkDbName = fkColBuilder.build().name;
|
|
2136
|
+
const childCompiled = childTable.compileValuesWithCasts(childSchema, config.children.rows, config.children.foreignKey);
|
|
2137
|
+
const childColNames = childCompiled.columns.map(c => `"${c.dbName}"`).join(', ');
|
|
2138
|
+
const childSelectCols = childCompiled.columns.map(c => `v."${c.dbName}"`).join(', ');
|
|
2139
|
+
const childValuesRows = childCompiled.valueRows.map((row, ix) => `(${ix}, ${row})`);
|
|
2140
|
+
const childOffset = params.length;
|
|
2141
|
+
const childValueList = childValuesRows.join(', ');
|
|
2142
|
+
let childSql = `INSERT INTO ${childTable._getQualifiedTableName()} ("${fkDbName}", ${childColNames})
|
|
2143
|
+
SELECT p."${parentPkDbName}", ${childSelectCols} FROM "__iwc_parent__" p CROSS JOIN (VALUES ${childValueList}) AS v("__iwc_ord", ${childColNames})
|
|
2144
|
+
ORDER BY v."__iwc_ord"`;
|
|
2145
|
+
childSql = childOffset === 0 ? childSql : (0, sql_utils_1.renumberPlaceholders)(childSql, childOffset);
|
|
2146
|
+
params.push(...childCompiled.params);
|
|
2147
|
+
// ---- returning assembly ----
|
|
2148
|
+
const childPkEntries = Object.entries(childSchema.columns).filter(([, colBuilder]) => colBuilder.build().primaryKey);
|
|
2149
|
+
const childPkDbName = childPkEntries.length === 1 ? childPkEntries[0][1].build().name : null;
|
|
2150
|
+
const prefixCtes = `"__iwc_parent__" AS (
|
|
2151
|
+
${parentSql}
|
|
2152
|
+
)`;
|
|
2153
|
+
const extraJoins = ['CROSS JOIN "__iwc_parent__" AS "__iwc_parent_j__"'];
|
|
2154
|
+
const extraSelects = parentSelCols.map(c => `"__iwc_parent_j__"."${c.dbName}" AS "__iwc_parent__.${c.prop}"`);
|
|
2155
|
+
const navigationInfo = childTable.detectNavigationInReturning(config.returning.children);
|
|
2156
|
+
let rawRows;
|
|
2157
|
+
let mapChildren;
|
|
2158
|
+
if (navigationInfo) {
|
|
2159
|
+
const built = childTable.buildReturningWithNavigation(childSql, params, config.returning.children, navigationInfo, {
|
|
2160
|
+
prefixCtes,
|
|
2161
|
+
extraJoins,
|
|
2162
|
+
extraSelects,
|
|
2163
|
+
extraCteReturningCols: childPkDbName ? [childPkDbName] : [],
|
|
2164
|
+
orderByCteColumn: childPkDbName ?? undefined,
|
|
2165
|
+
joinTableOverrides: new Map([[
|
|
2166
|
+
parentSchema.name,
|
|
2167
|
+
'__iwc_parent__',
|
|
2168
|
+
]]),
|
|
2169
|
+
});
|
|
2170
|
+
const result = executor ? await executor.query(built.sql, built.params) : await client.query(built.sql, built.params);
|
|
2171
|
+
rawRows = result.rows;
|
|
2172
|
+
mapChildren = stripped => childTable.mapReturningResultsWithNavigation(stripped, navigationInfo.navigationFields, built.nestedPaths);
|
|
2173
|
+
}
|
|
2174
|
+
else {
|
|
2175
|
+
const returningClause = childTable.buildReturningClause(config.returning.children);
|
|
2176
|
+
const pkExtra = childPkDbName ? `, "${childPkDbName}" AS "__iwc_child_pk__"` : '';
|
|
2177
|
+
const orderBy = childPkDbName ? '\nORDER BY "__mutation__"."__iwc_child_pk__"' : '';
|
|
2178
|
+
const sql = `WITH ${prefixCtes},
|
|
2179
|
+
"__mutation__" AS (
|
|
2180
|
+
${childSql}
|
|
2181
|
+
RETURNING ${returningClause.sql}${pkExtra}
|
|
2182
|
+
)
|
|
2183
|
+
SELECT "__mutation__".*, ${extraSelects.join(', ')}
|
|
2184
|
+
FROM "__mutation__"
|
|
2185
|
+
${extraJoins.join('\n')}${orderBy}`;
|
|
2186
|
+
const result = executor ? await executor.query(sql, params) : await client.query(sql, params);
|
|
2187
|
+
rawRows = result.rows;
|
|
2188
|
+
mapChildren = stripped => childTable.mapReturningResults(stripped.map(({ __iwc_child_pk__: _pk, ...rest }) => rest), returningClause.aliasToProperty);
|
|
2189
|
+
}
|
|
2190
|
+
if (rawRows.length === 0) {
|
|
2191
|
+
// Guard suppressed the insert (children.rows is non-empty, so an inserted
|
|
2192
|
+
// parent always yields at least one row here).
|
|
2193
|
+
return { parent: null, children: [] };
|
|
2194
|
+
}
|
|
2195
|
+
const parentRow = {};
|
|
2196
|
+
for (const col of parentSelCols) {
|
|
2197
|
+
const raw = rawRows[0][`__iwc_parent__.${col.prop}`];
|
|
2198
|
+
parentRow[col.prop] = col.mapper ? col.mapper.fromDriver(raw) : raw;
|
|
2199
|
+
}
|
|
2200
|
+
const strippedRows = rawRows.map((row) => {
|
|
2201
|
+
const clean = {};
|
|
2202
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2203
|
+
if (!key.startsWith('__iwc_parent__.')) {
|
|
2204
|
+
clean[key] = value;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return clean;
|
|
2208
|
+
});
|
|
2209
|
+
return { parent: parentRow, children: mapChildren(strippedRows) };
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Bulk sibling of {@link insertWithChildren}: N parent rows plus their child rows in
|
|
2213
|
+
* ONE statement (a task-DAG persist — N task rows + one audit row each — used to
|
|
2214
|
+
* cost two). Parent identity mapping rides the SAME serial-ascend guarantee the
|
|
2215
|
+
* single-parent variant documents: the parent leg inserts `ORDER BY` an input
|
|
2216
|
+
* ordinal, so generated serial ids ascend in input-row order, and a
|
|
2217
|
+
* `row_number() OVER (ORDER BY pk)` CTE recovers each parent's input index for the
|
|
2218
|
+
* child join:
|
|
2219
|
+
*
|
|
2220
|
+
* WITH "__ibwc_parent__" AS (
|
|
2221
|
+
* INSERT INTO parent (cols)
|
|
2222
|
+
* SELECT cols FROM (VALUES (0, ...), (1, ...)) v("__ibwc_ord", cols)
|
|
2223
|
+
* ORDER BY v."__ibwc_ord" RETURNING *
|
|
2224
|
+
* ), "__ibwc_pord__" AS (
|
|
2225
|
+
* SELECT p.*, row_number() OVER (ORDER BY p."pk") - 1 AS "__ibwc_ord"
|
|
2226
|
+
* FROM "__ibwc_parent__" p
|
|
2227
|
+
* ), "__mutation__" AS (
|
|
2228
|
+
* INSERT INTO child ("fk", cols)
|
|
2229
|
+
* SELECT p."pk", v.cols
|
|
2230
|
+
* FROM (VALUES (0, pix, ...), ...) v("__ibwc_cord", "__ibwc_pix", cols)
|
|
2231
|
+
* JOIN "__ibwc_pord__" p ON p."__ibwc_ord" = v."__ibwc_pix"
|
|
2232
|
+
* ORDER BY v."__ibwc_cord" RETURNING <child cols>, "childPk"
|
|
2233
|
+
* )
|
|
2234
|
+
* SELECT "__mutation__".*, <parent cols + ordinal via the fk join>
|
|
2235
|
+
* FROM "__mutation__" JOIN "__ibwc_pord__" ON fk = pk ORDER BY child pk
|
|
2236
|
+
*
|
|
2237
|
+
* Single-statement atomicity: a failing leg rolls back both inserts. Parents come
|
|
2238
|
+
* back in input order; children in child-input order.
|
|
2239
|
+
*
|
|
2240
|
+
* v1 restrictions: FLAT returning selectors on BOTH sides (no navigation
|
|
2241
|
+
* projections); single-column auto/serial primary keys on both tables; no
|
|
2242
|
+
* `unlessExists` guard; child rows must NOT carry the foreign-key property; EVERY
|
|
2243
|
+
* parent must be referenced by at least one child (parents are returned through
|
|
2244
|
+
* the child join — a childless parent would insert but vanish from the result, so
|
|
2245
|
+
* it is rejected up front; use plain `insertBulk` for childless rows); the whole
|
|
2246
|
+
* shape must fit one statement (no chunking).
|
|
2247
|
+
*/
|
|
2248
|
+
insertBulkWithChildren(config) {
|
|
2249
|
+
const { rows: childRows, foreignKey } = config.children;
|
|
2250
|
+
if (config.rows.length === 0) {
|
|
2251
|
+
throw new Error('insertBulkWithChildren: rows must be non-empty');
|
|
2252
|
+
}
|
|
2253
|
+
if (childRows.length === 0) {
|
|
2254
|
+
throw new Error('insertBulkWithChildren: children.rows must be non-empty — use insertBulk for childless parents');
|
|
2255
|
+
}
|
|
2256
|
+
const referenced = new Set();
|
|
2257
|
+
for (let i = 0; i < childRows.length; i++) {
|
|
2258
|
+
const { parentIndex, row } = childRows[i];
|
|
2259
|
+
if (!Number.isInteger(parentIndex) || parentIndex < 0 || parentIndex >= config.rows.length) {
|
|
2260
|
+
throw new Error(`insertBulkWithChildren: child row at index ${i} has parentIndex ${parentIndex} outside 0..${config.rows.length - 1}`);
|
|
2261
|
+
}
|
|
2262
|
+
if (row[foreignKey] !== undefined) {
|
|
2263
|
+
throw new Error(`insertBulkWithChildren: child row at index ${i} carries the foreign-key property "${foreignKey}" — it is sourced from its inserted parent`);
|
|
2264
|
+
}
|
|
2265
|
+
referenced.add(parentIndex);
|
|
2266
|
+
}
|
|
2267
|
+
for (let i = 0; i < config.rows.length; i++) {
|
|
2268
|
+
if (!referenced.has(i)) {
|
|
2269
|
+
throw new Error(`insertBulkWithChildren: parent at index ${i} has no child row — parents return through the child join (v1); insert childless rows via insertBulk`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const columnCount = Math.max(1, Object.keys(childRows[0].row).length + 2);
|
|
2273
|
+
const singleStatementLimit = Math.floor(Math.floor(65535 / columnCount) * 0.6);
|
|
2274
|
+
if (childRows.length + config.rows.length > singleStatementLimit) {
|
|
2275
|
+
throw new Error(`insertBulkWithChildren: ${config.rows.length} parents + ${childRows.length} children exceed the ~${singleStatementLimit}-row single-statement budget`);
|
|
2276
|
+
}
|
|
2277
|
+
return this.executeInsertBulkWithChildren(config);
|
|
2278
|
+
}
|
|
2279
|
+
/** @internal Async body of {@link insertBulkWithChildren} (validation stays synchronous). */
|
|
2280
|
+
async executeInsertBulkWithChildren(config) {
|
|
2281
|
+
const parentSchema = this._getSchema();
|
|
2282
|
+
const executor = this._getExecutor();
|
|
2283
|
+
const client = this._getClient();
|
|
2284
|
+
const childTable = config.children.table;
|
|
2285
|
+
if (childTable._getClient() !== client || childTable._getExecutor() !== executor) {
|
|
2286
|
+
throw new Error('insertBulkWithChildren: the child table uses a different database client or transaction than the parent — both must share one connection context');
|
|
2287
|
+
}
|
|
2288
|
+
const pkEntries = Object.entries(parentSchema.columns).filter(([, colBuilder]) => colBuilder.build().primaryKey);
|
|
2289
|
+
if (pkEntries.length !== 1) {
|
|
2290
|
+
throw new Error('insertBulkWithChildren requires a single-column parent primary key');
|
|
2291
|
+
}
|
|
2292
|
+
const parentPkDbName = pkEntries[0][1].build().name;
|
|
2293
|
+
// ---- parent leg: ordinal-ordered VALUES so serial ids ascend in input order ----
|
|
2294
|
+
const parentCompiled = this.compileValuesWithCasts(parentSchema, config.rows, null);
|
|
2295
|
+
const parentColNames = parentCompiled.columns.map(c => `"${c.dbName}"`).join(', ');
|
|
2296
|
+
const parentSelectCols = parentCompiled.columns.map(c => `v."${c.dbName}"`).join(', ');
|
|
2297
|
+
const parentValueRows = parentCompiled.valueRows.map((row, ix) => `(${ix}, ${row})`).join(', ');
|
|
2298
|
+
const params = [...parentCompiled.params];
|
|
2299
|
+
const parentMock = this.createMockEntity();
|
|
2300
|
+
const parentSelection = config.returning.parents(parentMock);
|
|
2301
|
+
const parentSelCols = [];
|
|
2302
|
+
for (const [prop, field] of Object.entries(parentSelection)) {
|
|
2303
|
+
const tableAlias = field?.__tableAlias;
|
|
2304
|
+
const dbColumnName = field?.__dbColumnName;
|
|
2305
|
+
if (dbColumnName == null || (tableAlias && tableAlias !== parentSchema.name)) {
|
|
2306
|
+
throw new Error(`insertBulkWithChildren: parents returning supports flat parent columns only — "${prop}" is not one`);
|
|
2307
|
+
}
|
|
2308
|
+
const colEntry = Object.entries(parentSchema.columns).find(([, colBuilder]) => colBuilder.build().name === dbColumnName);
|
|
2309
|
+
parentSelCols.push({ prop, dbName: dbColumnName, mapper: colEntry ? colEntry[1].build().mapper : undefined });
|
|
2310
|
+
}
|
|
2311
|
+
const parentSql = `INSERT INTO ${this._getQualifiedTableName()} (${parentColNames})
|
|
2312
|
+
SELECT ${parentSelectCols} FROM (VALUES ${parentValueRows}) AS v("__ibwc_ord", ${parentColNames})
|
|
2313
|
+
ORDER BY v."__ibwc_ord"
|
|
2314
|
+
RETURNING *`;
|
|
2315
|
+
// ---- child leg: (childOrd, parentIx, cells) VALUES joined to the ordinal CTE ----
|
|
2316
|
+
const childSchema = childTable._getSchema();
|
|
2317
|
+
const fkColBuilder = childSchema.columns[config.children.foreignKey];
|
|
2318
|
+
if (!fkColBuilder) {
|
|
2319
|
+
throw new Error(`insertBulkWithChildren: unknown child foreign-key property "${config.children.foreignKey}"`);
|
|
2320
|
+
}
|
|
2321
|
+
const fkDbName = fkColBuilder.build().name;
|
|
2322
|
+
const childCompiled = childTable.compileValuesWithCasts(childSchema, config.children.rows.map(r => r.row), config.children.foreignKey);
|
|
2323
|
+
const childColNames = childCompiled.columns.map(c => `"${c.dbName}"`).join(', ');
|
|
2324
|
+
const childSelectCols = childCompiled.columns.map(c => `v."${c.dbName}"`).join(', ');
|
|
2325
|
+
const childValueRows = childCompiled.valueRows.map((row, ix) => `(${ix}, ${config.children.rows[ix].parentIndex}, ${row})`).join(', ');
|
|
2326
|
+
const childOffset = params.length;
|
|
2327
|
+
let childSql = `INSERT INTO ${childTable._getQualifiedTableName()} ("${fkDbName}", ${childColNames})
|
|
2328
|
+
SELECT p."${parentPkDbName}", ${childSelectCols}
|
|
2329
|
+
FROM (VALUES ${childValueRows}) AS v("__ibwc_cord", "__ibwc_pix", ${childColNames})
|
|
2330
|
+
JOIN "__ibwc_pord__" p ON p."__ibwc_ord" = v."__ibwc_pix"
|
|
2331
|
+
ORDER BY v."__ibwc_cord"`;
|
|
2332
|
+
childSql = childOffset === 0 ? childSql : (0, sql_utils_1.renumberPlaceholders)(childSql, childOffset);
|
|
2333
|
+
params.push(...childCompiled.params);
|
|
2334
|
+
// ---- assembly: flat child returning + parent cols through the fk join ----
|
|
2335
|
+
if (childTable.detectNavigationInReturning(config.returning.children)) {
|
|
2336
|
+
throw new Error('insertBulkWithChildren: children returning supports flat child columns only (v1)');
|
|
2337
|
+
}
|
|
2338
|
+
const childPkEntries = Object.entries(childSchema.columns).filter(([, colBuilder]) => colBuilder.build().primaryKey);
|
|
2339
|
+
if (childPkEntries.length !== 1) {
|
|
2340
|
+
throw new Error('insertBulkWithChildren requires a single-column child primary key');
|
|
2341
|
+
}
|
|
2342
|
+
const childPkDbName = childPkEntries[0][1].build().name;
|
|
2343
|
+
const returningClause = childTable.buildReturningClause(config.returning.children);
|
|
2344
|
+
const parentJoinSelects = [
|
|
2345
|
+
`"__ibwc_pj__"."__ibwc_ord" AS "__ibwc_parent__.__ord"`,
|
|
2346
|
+
...parentSelCols.map(c => `"__ibwc_pj__"."${c.dbName}" AS "__ibwc_parent__.${c.prop}"`),
|
|
2347
|
+
];
|
|
2348
|
+
const sql = `WITH "__ibwc_parent__" AS (
|
|
2349
|
+
${parentSql}
|
|
2350
|
+
),
|
|
2351
|
+
"__ibwc_pord__" AS (
|
|
2352
|
+
SELECT p.*, row_number() OVER (ORDER BY p."${parentPkDbName}") - 1 AS "__ibwc_ord"
|
|
2353
|
+
FROM "__ibwc_parent__" p
|
|
2354
|
+
),
|
|
2355
|
+
"__mutation__" AS (
|
|
2356
|
+
${childSql}
|
|
2357
|
+
RETURNING ${returningClause.sql}, "${fkDbName}" AS "__ibwc_child_fk__", "${childPkDbName}" AS "__ibwc_child_pk__"
|
|
2358
|
+
)
|
|
2359
|
+
SELECT "__mutation__".*, ${parentJoinSelects.join(', ')}
|
|
2360
|
+
FROM "__mutation__"
|
|
2361
|
+
JOIN "__ibwc_pord__" "__ibwc_pj__" ON "__ibwc_pj__"."${parentPkDbName}" = "__mutation__"."__ibwc_child_fk__"
|
|
2362
|
+
ORDER BY "__mutation__"."__ibwc_child_pk__"`;
|
|
2363
|
+
const result = executor ? await executor.query(sql, params) : await client.query(sql, params);
|
|
2364
|
+
const rawRows = result.rows;
|
|
2365
|
+
const parentsByOrd = new Map();
|
|
2366
|
+
const strippedRows = [];
|
|
2367
|
+
for (const row of rawRows) {
|
|
2368
|
+
const ord = Number(row['__ibwc_parent__.__ord']);
|
|
2369
|
+
if (!parentsByOrd.has(ord)) {
|
|
2370
|
+
const parentRow = {};
|
|
2371
|
+
for (const col of parentSelCols) {
|
|
2372
|
+
const raw = row[`__ibwc_parent__.${col.prop}`];
|
|
2373
|
+
parentRow[col.prop] = col.mapper ? col.mapper.fromDriver(raw) : raw;
|
|
2374
|
+
}
|
|
2375
|
+
parentsByOrd.set(ord, parentRow);
|
|
2376
|
+
}
|
|
2377
|
+
const clean = {};
|
|
2378
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2379
|
+
if (!key.startsWith('__ibwc_parent__.') && key !== '__ibwc_child_fk__' && key !== '__ibwc_child_pk__') {
|
|
2380
|
+
clean[key] = value;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
strippedRows.push(clean);
|
|
2384
|
+
}
|
|
2385
|
+
const parents = [...parentsByOrd.entries()].sort((a, b) => a[0] - b[0]).map(([, parentRow]) => parentRow);
|
|
2386
|
+
const children = childTable.mapReturningResults(strippedRows, returningClause.aliasToProperty);
|
|
2387
|
+
return { parents, children };
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Compile rows into a cast-annotated VALUES fragment (`$n::type` / `NULL::type`
|
|
2391
|
+
* per cell — the bulkUpdate technique, so a bare `VALUES` source keeps correct
|
|
2392
|
+
* column types) with the same column-selection rules as {@link insertBulkSingle}.
|
|
2393
|
+
* @internal
|
|
2394
|
+
*/
|
|
2395
|
+
compileValuesWithCasts(schema, data, excludeProp) {
|
|
2396
|
+
const columns = [];
|
|
2397
|
+
for (const [propName, colBuilder] of Object.entries(schema.columns)) {
|
|
2398
|
+
if (propName === excludeProp) {
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
const colConfig = colBuilder.build();
|
|
2402
|
+
if (colConfig.autoIncrement) {
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
const hasDefinedValue = data.some(record => record[propName] !== undefined);
|
|
2406
|
+
if (!hasDefinedValue) {
|
|
2407
|
+
if (colConfig.default !== undefined || colConfig.identity) {
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const isPresentInAnyRow = data.some(record => propName in record);
|
|
2411
|
+
if (!isPresentInAnyRow) {
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
columns.push({
|
|
2416
|
+
propName,
|
|
2417
|
+
dbName: colConfig.name,
|
|
2418
|
+
pgType: DbEntityTable.PG_TYPE_MAP[colConfig.type] || colConfig.type,
|
|
2419
|
+
mapper: colConfig.mapper,
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
if (columns.length === 0) {
|
|
2423
|
+
throw new Error('insertWithChildren: rows resolve to zero insertable columns');
|
|
2424
|
+
}
|
|
2425
|
+
const valueRows = [];
|
|
2426
|
+
const params = [];
|
|
2427
|
+
let paramIndex = 1;
|
|
2428
|
+
for (const record of data) {
|
|
2429
|
+
const cells = [];
|
|
2430
|
+
for (const col of columns) {
|
|
2431
|
+
const rawValue = record[col.propName];
|
|
2432
|
+
const normalized = rawValue === undefined ? null : rawValue;
|
|
2433
|
+
const mapped = col.mapper ? col.mapper.toDriver(normalized) : normalized;
|
|
2434
|
+
if (mapped === undefined || mapped === null) {
|
|
2435
|
+
cells.push(`NULL::${col.pgType}`);
|
|
2436
|
+
}
|
|
2437
|
+
else {
|
|
2438
|
+
cells.push(`$${paramIndex++}::${col.pgType}`);
|
|
2439
|
+
params.push(mapped);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
valueRows.push(cells.join(', '));
|
|
2443
|
+
}
|
|
2444
|
+
return { columns, valueRows, params };
|
|
2445
|
+
}
|
|
2025
2446
|
/**
|
|
2026
2447
|
* Execute a single bulk insert batch
|
|
2027
2448
|
* @internal
|
|
2028
2449
|
*/
|
|
2029
2450
|
async insertBulkSingle(data, returning, overridingSystemValue, onConflictDoNothing) {
|
|
2030
|
-
const schema = this._getSchema();
|
|
2031
2451
|
const executor = this._getExecutor();
|
|
2032
2452
|
const client = this._getClient();
|
|
2453
|
+
const built = this._buildInsertBulkStatement(data, overridingSystemValue, onConflictDoNothing);
|
|
2454
|
+
if (!built) {
|
|
2455
|
+
return returning === undefined ? undefined : [];
|
|
2456
|
+
}
|
|
2457
|
+
// Check if RETURNING uses navigation properties
|
|
2458
|
+
const navigationInfo = returning && returning !== true && typeof returning === 'function'
|
|
2459
|
+
? this.detectNavigationInReturning(returning)
|
|
2460
|
+
: null;
|
|
2461
|
+
if (navigationInfo) {
|
|
2462
|
+
// Use CTE-based approach for navigation properties
|
|
2463
|
+
const { sql, params: queryParams, nestedPaths } = this.buildReturningWithNavigation(built.sql, built.params, returning, navigationInfo);
|
|
2464
|
+
const result = executor
|
|
2465
|
+
? await executor.query(sql, queryParams)
|
|
2466
|
+
: await client.query(sql, queryParams);
|
|
2467
|
+
return this.mapReturningResultsWithNavigation(result.rows, navigationInfo.navigationFields, nestedPaths);
|
|
2468
|
+
}
|
|
2469
|
+
// Standard RETURNING (no navigation properties)
|
|
2470
|
+
const returningClause = this.buildReturningClause(returning);
|
|
2471
|
+
let sql = built.sql;
|
|
2472
|
+
if (returningClause) {
|
|
2473
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
2474
|
+
}
|
|
2475
|
+
const result = executor
|
|
2476
|
+
? await executor.query(sql, built.params)
|
|
2477
|
+
: await client.query(sql, built.params);
|
|
2478
|
+
if (!returningClause) {
|
|
2479
|
+
return undefined;
|
|
2480
|
+
}
|
|
2481
|
+
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Builds the bare `INSERT ... VALUES` statement (no RETURNING clause) for ONE
|
|
2485
|
+
* chunk of rows — exactly the SQL {@link insertBulkSingle} executes, exposed
|
|
2486
|
+
* separately so `MutationBatch` can compose it as a data-modifying CTE leg.
|
|
2487
|
+
* Returns null when the rows resolve to zero insertable columns (or no rows).
|
|
2488
|
+
* @internal
|
|
2489
|
+
*/
|
|
2490
|
+
_buildInsertBulkStatement(data, overridingSystemValue, onConflictDoNothing) {
|
|
2491
|
+
if (data.length === 0) {
|
|
2492
|
+
return null;
|
|
2493
|
+
}
|
|
2494
|
+
const schema = this._getSchema();
|
|
2033
2495
|
const qualifiedTableName = this._getQualifiedTableName();
|
|
2034
2496
|
// Get columns from all rows - a column is included if ANY row has a non-undefined value for it
|
|
2035
2497
|
const columnConfigs = [];
|
|
@@ -2063,6 +2525,9 @@ class DbEntityTable {
|
|
|
2063
2525
|
mapper: config.mapper,
|
|
2064
2526
|
});
|
|
2065
2527
|
}
|
|
2528
|
+
if (columnConfigs.length === 0) {
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2066
2531
|
// Build VALUES clauses
|
|
2067
2532
|
const valuesClauses = [];
|
|
2068
2533
|
const params = [];
|
|
@@ -2080,28 +2545,6 @@ class DbEntityTable {
|
|
|
2080
2545
|
valuesClauses.push(`(${rowValues.join(', ')})`);
|
|
2081
2546
|
}
|
|
2082
2547
|
const columnList = columnConfigs.map(c => `"${c.dbName}"`).join(', ');
|
|
2083
|
-
// Check if RETURNING uses navigation properties
|
|
2084
|
-
const navigationInfo = returning && returning !== true && typeof returning === 'function'
|
|
2085
|
-
? this.detectNavigationInReturning(returning)
|
|
2086
|
-
: null;
|
|
2087
|
-
if (navigationInfo) {
|
|
2088
|
-
// Use CTE-based approach for navigation properties
|
|
2089
|
-
let insertSql = `INSERT INTO ${qualifiedTableName} (${columnList})`;
|
|
2090
|
-
if (overridingSystemValue) {
|
|
2091
|
-
insertSql += ' OVERRIDING SYSTEM VALUE';
|
|
2092
|
-
}
|
|
2093
|
-
insertSql += ` VALUES ${valuesClauses.join(', ')}`;
|
|
2094
|
-
if (onConflictDoNothing) {
|
|
2095
|
-
insertSql += ' ON CONFLICT DO NOTHING';
|
|
2096
|
-
}
|
|
2097
|
-
const { sql, params: queryParams, nestedPaths } = this.buildReturningWithNavigation(insertSql, params, returning, navigationInfo);
|
|
2098
|
-
const result = executor
|
|
2099
|
-
? await executor.query(sql, queryParams)
|
|
2100
|
-
: await client.query(sql, queryParams);
|
|
2101
|
-
return this.mapReturningResultsWithNavigation(result.rows, navigationInfo.navigationFields, nestedPaths);
|
|
2102
|
-
}
|
|
2103
|
-
// Standard RETURNING (no navigation properties)
|
|
2104
|
-
const returningClause = this.buildReturningClause(returning);
|
|
2105
2548
|
let sql = `INSERT INTO ${qualifiedTableName} (${columnList})`;
|
|
2106
2549
|
if (overridingSystemValue) {
|
|
2107
2550
|
sql += ' OVERRIDING SYSTEM VALUE';
|
|
@@ -2110,16 +2553,33 @@ class DbEntityTable {
|
|
|
2110
2553
|
if (onConflictDoNothing) {
|
|
2111
2554
|
sql += ' ON CONFLICT DO NOTHING';
|
|
2112
2555
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2556
|
+
return { sql, params };
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Builds a bare `DELETE FROM t WHERE "col" IN ($1, …)` statement (no
|
|
2560
|
+
* RETURNING clause) so `MutationBatch` can compose it as a data-modifying
|
|
2561
|
+
* CTE leg. Each value runs through the column's toDriver mapper — the same
|
|
2562
|
+
* fidelity rule the insert/update legs follow. Returns null for an empty
|
|
2563
|
+
* values array (mirroring the other leg builders' empty-input semantics);
|
|
2564
|
+
* an unknown column property throws at build time.
|
|
2565
|
+
* @internal
|
|
2566
|
+
*/
|
|
2567
|
+
_buildDeleteWhereInStatement(field, values) {
|
|
2568
|
+
if (values.length === 0) {
|
|
2569
|
+
return null;
|
|
2115
2570
|
}
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
return undefined;
|
|
2571
|
+
const schema = this._getSchema();
|
|
2572
|
+
const colBuilder = schema.columns[field];
|
|
2573
|
+
if (!colBuilder) {
|
|
2574
|
+
throw new Error(`deleteWhereIn: unknown column property "${field}" on entity "${schema.name}"`);
|
|
2121
2575
|
}
|
|
2122
|
-
|
|
2576
|
+
const config = colBuilder.build();
|
|
2577
|
+
const params = values.map(value => (config.mapper ? config.mapper.toDriver(value) : value));
|
|
2578
|
+
const placeholders = params.map((_, ix) => `$${ix + 1}`).join(', ');
|
|
2579
|
+
return {
|
|
2580
|
+
sql: `DELETE FROM ${this._getQualifiedTableName()} WHERE "${config.name}" IN (${placeholders})`,
|
|
2581
|
+
params,
|
|
2582
|
+
};
|
|
2123
2583
|
}
|
|
2124
2584
|
/**
|
|
2125
2585
|
* Upsert with advanced configuration
|
|
@@ -2238,13 +2698,13 @@ class DbEntityTable {
|
|
|
2238
2698
|
};
|
|
2239
2699
|
}
|
|
2240
2700
|
/**
|
|
2241
|
-
*
|
|
2701
|
+
* The bare `INSERT .. ON CONFLICT` assembly (no RETURNING) shared by
|
|
2702
|
+
* {@link upsertBulkSingle} and the MutationBatch upsert-leg compile —
|
|
2703
|
+
* one definition so the two can never drift.
|
|
2242
2704
|
* @internal
|
|
2243
2705
|
*/
|
|
2244
|
-
|
|
2706
|
+
buildUpsertStatementCore(values, primaryKeys, updateColumns, updateColumnFilter, overridingSystemValue, targetWhere, setWhere) {
|
|
2245
2707
|
const schema = this._getSchema();
|
|
2246
|
-
const executor = this._getExecutor();
|
|
2247
|
-
const client = this._getClient();
|
|
2248
2708
|
const qualifiedTableName = this._getQualifiedTableName();
|
|
2249
2709
|
// Extract all unique column names from all data objects
|
|
2250
2710
|
const columnConfigs = [];
|
|
@@ -2349,6 +2809,123 @@ class DbEntityTable {
|
|
|
2349
2809
|
sql += ` WHERE ${setWhere}`;
|
|
2350
2810
|
}
|
|
2351
2811
|
}
|
|
2812
|
+
return { sql, params };
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Builds a bare `UPDATE t SET .. WHERE "col" IN ($1, …)` statement (no
|
|
2816
|
+
* RETURNING) so `MutationBatch.addUpdateWhereIn` can compose it as a
|
|
2817
|
+
* data-modifying-CTE leg. SET semantics mirror the fluent `update()`:
|
|
2818
|
+
* plain values run through column mappers, `SqlFragment` values inline
|
|
2819
|
+
* with their params merged, and the lambda form resolves column refs
|
|
2820
|
+
* against this table's mock row. Returns null for an empty values array.
|
|
2821
|
+
* @internal
|
|
2822
|
+
*/
|
|
2823
|
+
_buildUpdateWhereInStatement(field, values, set) {
|
|
2824
|
+
if (values.length === 0) {
|
|
2825
|
+
return null;
|
|
2826
|
+
}
|
|
2827
|
+
const schema = this._getSchema();
|
|
2828
|
+
const whereColBuilder = schema.columns[field];
|
|
2829
|
+
if (!whereColBuilder) {
|
|
2830
|
+
throw new Error(`updateWhereIn: unknown column property "${field}" on entity "${schema.name}"`);
|
|
2831
|
+
}
|
|
2832
|
+
const whereConfig = whereColBuilder.build();
|
|
2833
|
+
const resolvedSet = typeof set === 'function' ? set(this.createMockEntity()) : set;
|
|
2834
|
+
const setClauses = [];
|
|
2835
|
+
const params = [];
|
|
2836
|
+
let paramIndex = 1;
|
|
2837
|
+
for (const [key, value] of Object.entries(resolvedSet)) {
|
|
2838
|
+
const column = schema.columns[key];
|
|
2839
|
+
if (!column) {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
const config = column.build();
|
|
2843
|
+
if (value instanceof conditions_1.SqlFragment) {
|
|
2844
|
+
const sqlBuildContext = {
|
|
2845
|
+
paramCounter: paramIndex,
|
|
2846
|
+
params,
|
|
2847
|
+
};
|
|
2848
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
2849
|
+
paramIndex = sqlBuildContext.paramCounter;
|
|
2850
|
+
setClauses.push(`"${config.name}" = ${fragmentSql}`);
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
setClauses.push(`"${config.name}" = $${paramIndex++}`);
|
|
2854
|
+
params.push(config.mapper ? config.mapper.toDriver(value) : value);
|
|
2855
|
+
}
|
|
2856
|
+
if (setClauses.length === 0) {
|
|
2857
|
+
throw new Error(`updateWhereIn: no valid columns to update on entity "${schema.name}"`);
|
|
2858
|
+
}
|
|
2859
|
+
const whereParams = values.map(value => (whereConfig.mapper ? whereConfig.mapper.toDriver(value) : value));
|
|
2860
|
+
const placeholders = whereParams.map(() => `$${paramIndex++}`).join(', ');
|
|
2861
|
+
params.push(...whereParams);
|
|
2862
|
+
return {
|
|
2863
|
+
sql: `UPDATE ${this._getQualifiedTableName()} SET ${setClauses.join(', ')} WHERE "${whereConfig.name}" IN (${placeholders})`,
|
|
2864
|
+
params,
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Bare `INSERT .. ON CONFLICT` compile (no RETURNING) for the MutationBatch
|
|
2869
|
+
* upsert leg — the {@link buildUpsertStatementCore} assembly with a narrow
|
|
2870
|
+
* plain config (prop-name primaryKey + updateColumns; no chunking, no
|
|
2871
|
+
* targetWhere/setWhere/system-value overrides in v1).
|
|
2872
|
+
* @internal
|
|
2873
|
+
*/
|
|
2874
|
+
_buildUpsertBulkStatement(values, config) {
|
|
2875
|
+
if (values.length === 0) {
|
|
2876
|
+
return null;
|
|
2877
|
+
}
|
|
2878
|
+
const primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
|
|
2879
|
+
return this.buildUpsertStatementCore(values, primaryKeys, config.updateColumns, undefined, false, undefined, undefined);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Bare `INSERT INTO t (..) SELECT <cells> FROM "__MB_PARENT__" WHERE
|
|
2883
|
+
* "__MB_PARENT__"."<col>" <> $n` compile for the MutationBatch dependent
|
|
2884
|
+
* leg — a single row inserted iff the parent leg's exposed column differs
|
|
2885
|
+
* from the sentinel (the conditional-audit-log shape). Cells reuse the
|
|
2886
|
+
* insertWithChildren `$n::type` cast technique so types survive without a
|
|
2887
|
+
* VALUES-derived table; the `__MB_PARENT__` token is rewritten to the
|
|
2888
|
+
* parent's actual CTE name at batch assembly.
|
|
2889
|
+
* @internal
|
|
2890
|
+
*/
|
|
2891
|
+
_buildDependentInsertSelectStatement(row, whereColumnAlias, whereNotEquals) {
|
|
2892
|
+
const schema = this._getSchema();
|
|
2893
|
+
const compiled = this.compileValuesWithCasts(schema, [row], null);
|
|
2894
|
+
const columnList = compiled.columns.map(c => `"${c.dbName}"`).join(', ');
|
|
2895
|
+
const params = [...compiled.params];
|
|
2896
|
+
const sentinelIndex = params.length + 1;
|
|
2897
|
+
params.push(whereNotEquals);
|
|
2898
|
+
return {
|
|
2899
|
+
sql: `INSERT INTO ${this._getQualifiedTableName()} (${columnList}) SELECT ${compiled.valueRows[0]} FROM "__MB_PARENT__" WHERE "__MB_PARENT__"."${whereColumnAlias}" <> $${sentinelIndex}`,
|
|
2900
|
+
params,
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Resolves entity property names to their DB column names (throws on an
|
|
2905
|
+
* unknown property) — the MutationBatch expose/returning lists come in as
|
|
2906
|
+
* prop names and compile to quoted DB identifiers.
|
|
2907
|
+
* @internal
|
|
2908
|
+
*/
|
|
2909
|
+
_resolveColumnDbNames(props) {
|
|
2910
|
+
const schema = this._getSchema();
|
|
2911
|
+
return props.map((prop) => {
|
|
2912
|
+
const colBuilder = schema.columns[prop];
|
|
2913
|
+
if (!colBuilder) {
|
|
2914
|
+
throw new Error(`Unknown column property "${prop}" on entity "${schema.name}"`);
|
|
2915
|
+
}
|
|
2916
|
+
return { prop, dbName: colBuilder.build().name };
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Execute a single upsert batch
|
|
2921
|
+
* @internal
|
|
2922
|
+
*/
|
|
2923
|
+
async upsertBulkSingle(values, primaryKeys, updateColumns, updateColumnFilter, overridingSystemValue, targetWhere, setWhere, returning) {
|
|
2924
|
+
const executor = this._getExecutor();
|
|
2925
|
+
const client = this._getClient();
|
|
2926
|
+
const built = this.buildUpsertStatementCore(values, primaryKeys, updateColumns, updateColumnFilter, overridingSystemValue, targetWhere, setWhere);
|
|
2927
|
+
let sql = built.sql;
|
|
2928
|
+
const params = built.params;
|
|
2352
2929
|
// Check if RETURNING uses navigation properties
|
|
2353
2930
|
const navigationInfo = returning && returning !== true && typeof returning === 'function'
|
|
2354
2931
|
? this.detectNavigationInReturning(returning)
|
|
@@ -2825,32 +3402,7 @@ class DbEntityTable {
|
|
|
2825
3402
|
if (data.length === 0) {
|
|
2826
3403
|
return returning === undefined ? undefined : [];
|
|
2827
3404
|
}
|
|
2828
|
-
const
|
|
2829
|
-
// Determine primary keys
|
|
2830
|
-
let primaryKeys = [];
|
|
2831
|
-
if (config?.primaryKey) {
|
|
2832
|
-
primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
|
|
2833
|
-
}
|
|
2834
|
-
else {
|
|
2835
|
-
// Auto-detect from schema
|
|
2836
|
-
for (const [key, colBuilder] of Object.entries(schema.columns)) {
|
|
2837
|
-
const colConfig = colBuilder.build();
|
|
2838
|
-
if (colConfig.primaryKey) {
|
|
2839
|
-
primaryKeys.push(key);
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
if (primaryKeys.length === 0) {
|
|
2844
|
-
throw new Error('bulkUpdate requires at least one primary key column');
|
|
2845
|
-
}
|
|
2846
|
-
// Validate all records have primary keys
|
|
2847
|
-
for (let i = 0; i < data.length; i++) {
|
|
2848
|
-
for (const pk of primaryKeys) {
|
|
2849
|
-
if (data[i][pk] === undefined) {
|
|
2850
|
-
throw new Error(`Record at index ${i} is missing primary key "${pk}"`);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
3405
|
+
const primaryKeys = table._resolveBulkUpdatePrimaryKeys(data, config);
|
|
2854
3406
|
// Calculate chunk size
|
|
2855
3407
|
let chunkSize = config?.chunkSize;
|
|
2856
3408
|
if (chunkSize == null) {
|
|
@@ -2892,9 +3444,66 @@ class DbEntityTable {
|
|
|
2892
3444
|
* @internal
|
|
2893
3445
|
*/
|
|
2894
3446
|
async bulkUpdateSingle(data, primaryKeys, returning) {
|
|
2895
|
-
const schema = this._getSchema();
|
|
2896
3447
|
const executor = this._getExecutor();
|
|
2897
3448
|
const client = this._getClient();
|
|
3449
|
+
const built = this._buildBulkUpdateStatement(data, primaryKeys);
|
|
3450
|
+
// Build RETURNING clause
|
|
3451
|
+
const returningClause = this.buildReturningClause(returning, 't');
|
|
3452
|
+
let sql = built.sql;
|
|
3453
|
+
if (returningClause) {
|
|
3454
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
3455
|
+
}
|
|
3456
|
+
const result = executor
|
|
3457
|
+
? await executor.query(sql, built.params)
|
|
3458
|
+
: await client.query(sql, built.params);
|
|
3459
|
+
if (!returningClause) {
|
|
3460
|
+
return undefined;
|
|
3461
|
+
}
|
|
3462
|
+
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Resolves + validates the primary key columns a bulkUpdate matches rows on —
|
|
3466
|
+
* shared by {@link bulkUpdate} and `MutationBatch`.
|
|
3467
|
+
* @internal
|
|
3468
|
+
*/
|
|
3469
|
+
_resolveBulkUpdatePrimaryKeys(data, config) {
|
|
3470
|
+
const schema = this._getSchema();
|
|
3471
|
+
// Determine primary keys
|
|
3472
|
+
let primaryKeys = [];
|
|
3473
|
+
if (config?.primaryKey) {
|
|
3474
|
+
primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
|
|
3475
|
+
}
|
|
3476
|
+
else {
|
|
3477
|
+
// Auto-detect from schema
|
|
3478
|
+
for (const [key, colBuilder] of Object.entries(schema.columns)) {
|
|
3479
|
+
const colConfig = colBuilder.build();
|
|
3480
|
+
if (colConfig.primaryKey) {
|
|
3481
|
+
primaryKeys.push(key);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
if (primaryKeys.length === 0) {
|
|
3486
|
+
throw new Error('bulkUpdate requires at least one primary key column');
|
|
3487
|
+
}
|
|
3488
|
+
// Validate all records have primary keys
|
|
3489
|
+
for (let i = 0; i < data.length; i++) {
|
|
3490
|
+
for (const pk of primaryKeys) {
|
|
3491
|
+
if (data[i][pk] === undefined) {
|
|
3492
|
+
throw new Error(`Record at index ${i} is missing primary key "${pk}"`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
return primaryKeys;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Builds the bare `UPDATE ... FROM (VALUES ...)` statement (no RETURNING
|
|
3500
|
+
* clause) for ONE chunk of rows — exactly the SQL {@link bulkUpdateSingle}
|
|
3501
|
+
* executes (per-row `"col__provided"` CASE flags included), exposed
|
|
3502
|
+
* separately so `MutationBatch` can compose it as a data-modifying CTE leg.
|
|
3503
|
+
* @internal
|
|
3504
|
+
*/
|
|
3505
|
+
_buildBulkUpdateStatement(data, primaryKeys) {
|
|
3506
|
+
const schema = this._getSchema();
|
|
2898
3507
|
const qualifiedTableName = this._getQualifiedTableName();
|
|
2899
3508
|
const primaryKeySet = new Set(primaryKeys);
|
|
2900
3509
|
// Single pass: collect columns and build column info simultaneously
|
|
@@ -2969,23 +3578,12 @@ class DbEntityTable {
|
|
|
2969
3578
|
}
|
|
2970
3579
|
valuesClauses.push(`(${rowValues.join(', ')})`);
|
|
2971
3580
|
}
|
|
2972
|
-
|
|
2973
|
-
const returningClause = this.buildReturningClause(returning, 't');
|
|
2974
|
-
let sql = `
|
|
3581
|
+
const sql = `
|
|
2975
3582
|
UPDATE ${qualifiedTableName} AS t
|
|
2976
3583
|
SET ${setClauses.join(', ')}
|
|
2977
3584
|
FROM (VALUES ${valuesClauses.join(', ')}) AS v(${valueColumnList})
|
|
2978
3585
|
WHERE ${whereClause}`.trim();
|
|
2979
|
-
|
|
2980
|
-
sql += ` RETURNING ${returningClause.sql}`;
|
|
2981
|
-
}
|
|
2982
|
-
const result = executor
|
|
2983
|
-
? await executor.query(sql, params)
|
|
2984
|
-
: await client.query(sql, params);
|
|
2985
|
-
if (!returningClause) {
|
|
2986
|
-
return undefined;
|
|
2987
|
-
}
|
|
2988
|
-
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
3586
|
+
return { sql, params };
|
|
2989
3587
|
}
|
|
2990
3588
|
/**
|
|
2991
3589
|
* Delete all records from the table
|
|
@@ -3339,7 +3937,14 @@ WHERE ${whereClause}`.trim();
|
|
|
3339
3937
|
* Build RETURNING clause with navigation property support using CTE
|
|
3340
3938
|
* @internal
|
|
3341
3939
|
*/
|
|
3342
|
-
buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo
|
|
3940
|
+
buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo,
|
|
3941
|
+
/**
|
|
3942
|
+
* Composition hooks for `insertWithChildren`: CTEs prepended before
|
|
3943
|
+
* "__mutation__" (whose SQL may reference them), extra joins/select parts
|
|
3944
|
+
* on the outer statement, extra columns forced into the CTE's RETURNING
|
|
3945
|
+
* list, and a deterministic outer ORDER BY on a CTE column.
|
|
3946
|
+
*/
|
|
3947
|
+
options) {
|
|
3343
3948
|
const schema = this._getSchema();
|
|
3344
3949
|
const schemaRegistry = this._getSchemaRegistry();
|
|
3345
3950
|
const mainTableColumns = new Set();
|
|
@@ -3472,6 +4077,10 @@ WHERE ${whereClause}`.trim();
|
|
|
3472
4077
|
if (join.targetSchema) {
|
|
3473
4078
|
qualifiedJoinTable = `"${join.targetSchema}"."${join.targetTable}"`;
|
|
3474
4079
|
}
|
|
4080
|
+
const cteOverride = options?.joinTableOverrides?.get(join.targetTable);
|
|
4081
|
+
if (cteOverride) {
|
|
4082
|
+
qualifiedJoinTable = `"${cteOverride}"`;
|
|
4083
|
+
}
|
|
3475
4084
|
const joinConditions = [];
|
|
3476
4085
|
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
3477
4086
|
const fk = join.foreignKeys[i];
|
|
@@ -3496,12 +4105,16 @@ WHERE ${whereClause}`.trim();
|
|
|
3496
4105
|
for (const collection of collectionSubqueries) {
|
|
3497
4106
|
joinClauses.push(collection.joinClause);
|
|
3498
4107
|
}
|
|
3499
|
-
const
|
|
4108
|
+
const prefixCtes = options?.prefixCtes ? `${options.prefixCtes},\n` : '';
|
|
4109
|
+
const extraJoins = options?.extraJoins?.length ? `${options.extraJoins.join('\n')}\n` : '';
|
|
4110
|
+
const extraSelects = options?.extraSelects?.length ? `, ${options.extraSelects.join(', ')}` : '';
|
|
4111
|
+
const orderBy = options?.orderByCteColumn ? `\nORDER BY "__mutation__"."${options.orderByCteColumn}"` : '';
|
|
4112
|
+
const sql = `WITH ${prefixCtes}"__mutation__" AS (
|
|
3500
4113
|
${mutationWithReturning}
|
|
3501
4114
|
)
|
|
3502
|
-
SELECT ${selectParts.join(', ')}
|
|
4115
|
+
SELECT ${selectParts.join(', ')}${extraSelects}
|
|
3503
4116
|
FROM "__mutation__"
|
|
3504
|
-
${joinClauses.join('\n')}`;
|
|
4117
|
+
${extraJoins}${joinClauses.join('\n')}${orderBy}`;
|
|
3505
4118
|
return { sql, params: allParams, nestedPaths };
|
|
3506
4119
|
}
|
|
3507
4120
|
/**
|