linkgress-orm 0.4.54 → 0.4.56
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 +100 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +386 -94
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/query/mutation-batch.d.ts +101 -0
- package/dist/query/mutation-batch.d.ts.map +1 -0
- package/dist/query/mutation-batch.js +154 -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.map +1 -1
- package/dist/query/query-builder.js +78 -31
- 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 +1 -1
|
@@ -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,297 @@ 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
|
+
* Compile rows into a cast-annotated VALUES fragment (`$n::type` / `NULL::type`
|
|
2213
|
+
* per cell — the bulkUpdate technique, so a bare `VALUES` source keeps correct
|
|
2214
|
+
* column types) with the same column-selection rules as {@link insertBulkSingle}.
|
|
2215
|
+
* @internal
|
|
2216
|
+
*/
|
|
2217
|
+
compileValuesWithCasts(schema, data, excludeProp) {
|
|
2218
|
+
const columns = [];
|
|
2219
|
+
for (const [propName, colBuilder] of Object.entries(schema.columns)) {
|
|
2220
|
+
if (propName === excludeProp) {
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const colConfig = colBuilder.build();
|
|
2224
|
+
if (colConfig.autoIncrement) {
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const hasDefinedValue = data.some(record => record[propName] !== undefined);
|
|
2228
|
+
if (!hasDefinedValue) {
|
|
2229
|
+
if (colConfig.default !== undefined || colConfig.identity) {
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
const isPresentInAnyRow = data.some(record => propName in record);
|
|
2233
|
+
if (!isPresentInAnyRow) {
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
columns.push({
|
|
2238
|
+
propName,
|
|
2239
|
+
dbName: colConfig.name,
|
|
2240
|
+
pgType: DbEntityTable.PG_TYPE_MAP[colConfig.type] || colConfig.type,
|
|
2241
|
+
mapper: colConfig.mapper,
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
if (columns.length === 0) {
|
|
2245
|
+
throw new Error('insertWithChildren: rows resolve to zero insertable columns');
|
|
2246
|
+
}
|
|
2247
|
+
const valueRows = [];
|
|
2248
|
+
const params = [];
|
|
2249
|
+
let paramIndex = 1;
|
|
2250
|
+
for (const record of data) {
|
|
2251
|
+
const cells = [];
|
|
2252
|
+
for (const col of columns) {
|
|
2253
|
+
const rawValue = record[col.propName];
|
|
2254
|
+
const normalized = rawValue === undefined ? null : rawValue;
|
|
2255
|
+
const mapped = col.mapper ? col.mapper.toDriver(normalized) : normalized;
|
|
2256
|
+
if (mapped === undefined || mapped === null) {
|
|
2257
|
+
cells.push(`NULL::${col.pgType}`);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
cells.push(`$${paramIndex++}::${col.pgType}`);
|
|
2261
|
+
params.push(mapped);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
valueRows.push(cells.join(', '));
|
|
2265
|
+
}
|
|
2266
|
+
return { columns, valueRows, params };
|
|
2267
|
+
}
|
|
2025
2268
|
/**
|
|
2026
2269
|
* Execute a single bulk insert batch
|
|
2027
2270
|
* @internal
|
|
2028
2271
|
*/
|
|
2029
2272
|
async insertBulkSingle(data, returning, overridingSystemValue, onConflictDoNothing) {
|
|
2030
|
-
const schema = this._getSchema();
|
|
2031
2273
|
const executor = this._getExecutor();
|
|
2032
2274
|
const client = this._getClient();
|
|
2275
|
+
const built = this._buildInsertBulkStatement(data, overridingSystemValue, onConflictDoNothing);
|
|
2276
|
+
if (!built) {
|
|
2277
|
+
return returning === undefined ? undefined : [];
|
|
2278
|
+
}
|
|
2279
|
+
// Check if RETURNING uses navigation properties
|
|
2280
|
+
const navigationInfo = returning && returning !== true && typeof returning === 'function'
|
|
2281
|
+
? this.detectNavigationInReturning(returning)
|
|
2282
|
+
: null;
|
|
2283
|
+
if (navigationInfo) {
|
|
2284
|
+
// Use CTE-based approach for navigation properties
|
|
2285
|
+
const { sql, params: queryParams, nestedPaths } = this.buildReturningWithNavigation(built.sql, built.params, returning, navigationInfo);
|
|
2286
|
+
const result = executor
|
|
2287
|
+
? await executor.query(sql, queryParams)
|
|
2288
|
+
: await client.query(sql, queryParams);
|
|
2289
|
+
return this.mapReturningResultsWithNavigation(result.rows, navigationInfo.navigationFields, nestedPaths);
|
|
2290
|
+
}
|
|
2291
|
+
// Standard RETURNING (no navigation properties)
|
|
2292
|
+
const returningClause = this.buildReturningClause(returning);
|
|
2293
|
+
let sql = built.sql;
|
|
2294
|
+
if (returningClause) {
|
|
2295
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
2296
|
+
}
|
|
2297
|
+
const result = executor
|
|
2298
|
+
? await executor.query(sql, built.params)
|
|
2299
|
+
: await client.query(sql, built.params);
|
|
2300
|
+
if (!returningClause) {
|
|
2301
|
+
return undefined;
|
|
2302
|
+
}
|
|
2303
|
+
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Builds the bare `INSERT ... VALUES` statement (no RETURNING clause) for ONE
|
|
2307
|
+
* chunk of rows — exactly the SQL {@link insertBulkSingle} executes, exposed
|
|
2308
|
+
* separately so `MutationBatch` can compose it as a data-modifying CTE leg.
|
|
2309
|
+
* Returns null when the rows resolve to zero insertable columns (or no rows).
|
|
2310
|
+
* @internal
|
|
2311
|
+
*/
|
|
2312
|
+
_buildInsertBulkStatement(data, overridingSystemValue, onConflictDoNothing) {
|
|
2313
|
+
if (data.length === 0) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
const schema = this._getSchema();
|
|
2033
2317
|
const qualifiedTableName = this._getQualifiedTableName();
|
|
2034
2318
|
// Get columns from all rows - a column is included if ANY row has a non-undefined value for it
|
|
2035
2319
|
const columnConfigs = [];
|
|
@@ -2063,6 +2347,9 @@ class DbEntityTable {
|
|
|
2063
2347
|
mapper: config.mapper,
|
|
2064
2348
|
});
|
|
2065
2349
|
}
|
|
2350
|
+
if (columnConfigs.length === 0) {
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2066
2353
|
// Build VALUES clauses
|
|
2067
2354
|
const valuesClauses = [];
|
|
2068
2355
|
const params = [];
|
|
@@ -2080,28 +2367,6 @@ class DbEntityTable {
|
|
|
2080
2367
|
valuesClauses.push(`(${rowValues.join(', ')})`);
|
|
2081
2368
|
}
|
|
2082
2369
|
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
2370
|
let sql = `INSERT INTO ${qualifiedTableName} (${columnList})`;
|
|
2106
2371
|
if (overridingSystemValue) {
|
|
2107
2372
|
sql += ' OVERRIDING SYSTEM VALUE';
|
|
@@ -2110,16 +2375,7 @@ class DbEntityTable {
|
|
|
2110
2375
|
if (onConflictDoNothing) {
|
|
2111
2376
|
sql += ' ON CONFLICT DO NOTHING';
|
|
2112
2377
|
}
|
|
2113
|
-
|
|
2114
|
-
sql += ` RETURNING ${returningClause.sql}`;
|
|
2115
|
-
}
|
|
2116
|
-
const result = executor
|
|
2117
|
-
? await executor.query(sql, params)
|
|
2118
|
-
: await client.query(sql, params);
|
|
2119
|
-
if (!returningClause) {
|
|
2120
|
-
return undefined;
|
|
2121
|
-
}
|
|
2122
|
-
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
2378
|
+
return { sql, params };
|
|
2123
2379
|
}
|
|
2124
2380
|
/**
|
|
2125
2381
|
* Upsert with advanced configuration
|
|
@@ -2825,32 +3081,7 @@ class DbEntityTable {
|
|
|
2825
3081
|
if (data.length === 0) {
|
|
2826
3082
|
return returning === undefined ? undefined : [];
|
|
2827
3083
|
}
|
|
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
|
-
}
|
|
3084
|
+
const primaryKeys = table._resolveBulkUpdatePrimaryKeys(data, config);
|
|
2854
3085
|
// Calculate chunk size
|
|
2855
3086
|
let chunkSize = config?.chunkSize;
|
|
2856
3087
|
if (chunkSize == null) {
|
|
@@ -2892,9 +3123,66 @@ class DbEntityTable {
|
|
|
2892
3123
|
* @internal
|
|
2893
3124
|
*/
|
|
2894
3125
|
async bulkUpdateSingle(data, primaryKeys, returning) {
|
|
2895
|
-
const schema = this._getSchema();
|
|
2896
3126
|
const executor = this._getExecutor();
|
|
2897
3127
|
const client = this._getClient();
|
|
3128
|
+
const built = this._buildBulkUpdateStatement(data, primaryKeys);
|
|
3129
|
+
// Build RETURNING clause
|
|
3130
|
+
const returningClause = this.buildReturningClause(returning, 't');
|
|
3131
|
+
let sql = built.sql;
|
|
3132
|
+
if (returningClause) {
|
|
3133
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
3134
|
+
}
|
|
3135
|
+
const result = executor
|
|
3136
|
+
? await executor.query(sql, built.params)
|
|
3137
|
+
: await client.query(sql, built.params);
|
|
3138
|
+
if (!returningClause) {
|
|
3139
|
+
return undefined;
|
|
3140
|
+
}
|
|
3141
|
+
return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Resolves + validates the primary key columns a bulkUpdate matches rows on —
|
|
3145
|
+
* shared by {@link bulkUpdate} and `MutationBatch`.
|
|
3146
|
+
* @internal
|
|
3147
|
+
*/
|
|
3148
|
+
_resolveBulkUpdatePrimaryKeys(data, config) {
|
|
3149
|
+
const schema = this._getSchema();
|
|
3150
|
+
// Determine primary keys
|
|
3151
|
+
let primaryKeys = [];
|
|
3152
|
+
if (config?.primaryKey) {
|
|
3153
|
+
primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
|
|
3154
|
+
}
|
|
3155
|
+
else {
|
|
3156
|
+
// Auto-detect from schema
|
|
3157
|
+
for (const [key, colBuilder] of Object.entries(schema.columns)) {
|
|
3158
|
+
const colConfig = colBuilder.build();
|
|
3159
|
+
if (colConfig.primaryKey) {
|
|
3160
|
+
primaryKeys.push(key);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (primaryKeys.length === 0) {
|
|
3165
|
+
throw new Error('bulkUpdate requires at least one primary key column');
|
|
3166
|
+
}
|
|
3167
|
+
// Validate all records have primary keys
|
|
3168
|
+
for (let i = 0; i < data.length; i++) {
|
|
3169
|
+
for (const pk of primaryKeys) {
|
|
3170
|
+
if (data[i][pk] === undefined) {
|
|
3171
|
+
throw new Error(`Record at index ${i} is missing primary key "${pk}"`);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
return primaryKeys;
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Builds the bare `UPDATE ... FROM (VALUES ...)` statement (no RETURNING
|
|
3179
|
+
* clause) for ONE chunk of rows — exactly the SQL {@link bulkUpdateSingle}
|
|
3180
|
+
* executes (per-row `"col__provided"` CASE flags included), exposed
|
|
3181
|
+
* separately so `MutationBatch` can compose it as a data-modifying CTE leg.
|
|
3182
|
+
* @internal
|
|
3183
|
+
*/
|
|
3184
|
+
_buildBulkUpdateStatement(data, primaryKeys) {
|
|
3185
|
+
const schema = this._getSchema();
|
|
2898
3186
|
const qualifiedTableName = this._getQualifiedTableName();
|
|
2899
3187
|
const primaryKeySet = new Set(primaryKeys);
|
|
2900
3188
|
// Single pass: collect columns and build column info simultaneously
|
|
@@ -2969,23 +3257,12 @@ class DbEntityTable {
|
|
|
2969
3257
|
}
|
|
2970
3258
|
valuesClauses.push(`(${rowValues.join(', ')})`);
|
|
2971
3259
|
}
|
|
2972
|
-
|
|
2973
|
-
const returningClause = this.buildReturningClause(returning, 't');
|
|
2974
|
-
let sql = `
|
|
3260
|
+
const sql = `
|
|
2975
3261
|
UPDATE ${qualifiedTableName} AS t
|
|
2976
3262
|
SET ${setClauses.join(', ')}
|
|
2977
3263
|
FROM (VALUES ${valuesClauses.join(', ')}) AS v(${valueColumnList})
|
|
2978
3264
|
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);
|
|
3265
|
+
return { sql, params };
|
|
2989
3266
|
}
|
|
2990
3267
|
/**
|
|
2991
3268
|
* Delete all records from the table
|
|
@@ -3339,7 +3616,14 @@ WHERE ${whereClause}`.trim();
|
|
|
3339
3616
|
* Build RETURNING clause with navigation property support using CTE
|
|
3340
3617
|
* @internal
|
|
3341
3618
|
*/
|
|
3342
|
-
buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo
|
|
3619
|
+
buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo,
|
|
3620
|
+
/**
|
|
3621
|
+
* Composition hooks for `insertWithChildren`: CTEs prepended before
|
|
3622
|
+
* "__mutation__" (whose SQL may reference them), extra joins/select parts
|
|
3623
|
+
* on the outer statement, extra columns forced into the CTE's RETURNING
|
|
3624
|
+
* list, and a deterministic outer ORDER BY on a CTE column.
|
|
3625
|
+
*/
|
|
3626
|
+
options) {
|
|
3343
3627
|
const schema = this._getSchema();
|
|
3344
3628
|
const schemaRegistry = this._getSchemaRegistry();
|
|
3345
3629
|
const mainTableColumns = new Set();
|
|
@@ -3472,6 +3756,10 @@ WHERE ${whereClause}`.trim();
|
|
|
3472
3756
|
if (join.targetSchema) {
|
|
3473
3757
|
qualifiedJoinTable = `"${join.targetSchema}"."${join.targetTable}"`;
|
|
3474
3758
|
}
|
|
3759
|
+
const cteOverride = options?.joinTableOverrides?.get(join.targetTable);
|
|
3760
|
+
if (cteOverride) {
|
|
3761
|
+
qualifiedJoinTable = `"${cteOverride}"`;
|
|
3762
|
+
}
|
|
3475
3763
|
const joinConditions = [];
|
|
3476
3764
|
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
3477
3765
|
const fk = join.foreignKeys[i];
|
|
@@ -3496,12 +3784,16 @@ WHERE ${whereClause}`.trim();
|
|
|
3496
3784
|
for (const collection of collectionSubqueries) {
|
|
3497
3785
|
joinClauses.push(collection.joinClause);
|
|
3498
3786
|
}
|
|
3499
|
-
const
|
|
3787
|
+
const prefixCtes = options?.prefixCtes ? `${options.prefixCtes},\n` : '';
|
|
3788
|
+
const extraJoins = options?.extraJoins?.length ? `${options.extraJoins.join('\n')}\n` : '';
|
|
3789
|
+
const extraSelects = options?.extraSelects?.length ? `, ${options.extraSelects.join(', ')}` : '';
|
|
3790
|
+
const orderBy = options?.orderByCteColumn ? `\nORDER BY "__mutation__"."${options.orderByCteColumn}"` : '';
|
|
3791
|
+
const sql = `WITH ${prefixCtes}"__mutation__" AS (
|
|
3500
3792
|
${mutationWithReturning}
|
|
3501
3793
|
)
|
|
3502
|
-
SELECT ${selectParts.join(', ')}
|
|
3794
|
+
SELECT ${selectParts.join(', ')}${extraSelects}
|
|
3503
3795
|
FROM "__mutation__"
|
|
3504
|
-
${joinClauses.join('\n')}`;
|
|
3796
|
+
${extraJoins}${joinClauses.join('\n')}${orderBy}`;
|
|
3505
3797
|
return { sql, params: allParams, nestedPaths };
|
|
3506
3798
|
}
|
|
3507
3799
|
/**
|