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.
@@ -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 sql_utils_1 = require("../query/sql-utils");
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, sql_utils_1.buildColumnNamesList)(this.schema, columns);
543
- const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
544
- const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
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, sql_utils_1.buildReturningColumnList)(this.schema);
757
- const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
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, sql_utils_1.calculateOptimalChunkSize)(columnCount, insertConfig?.chunkSize);
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, sql_utils_1.extractUniqueColumnKeys)(dataArray, this.schema, insertConfig?.overridingSystemValue);
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, sql_utils_1.buildColumnConfigs)(this.schema, columns, insertConfig?.overridingSystemValue);
806
- const { valueClauses, params } = (0, sql_utils_1.buildValuesClause)(dataArray, columnConfigs);
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, sql_utils_1.buildReturningColumnList)(this.schema);
809
- const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
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, sql_utils_1.detectPrimaryKeys)(this.schema);
842
+ : (0, sql_utils_2.detectPrimaryKeys)(this.schema);
842
843
  // Auto-detect overridingSystemValue
843
844
  const overridingSystemValue = config?.overridingSystemValue ??
844
- (0, sql_utils_1.hasAutoIncrementPrimaryKey)(this.schema, Object.keys(referenceItem));
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, sql_utils_1.calculateOptimalChunkSize)(columnCount, config?.chunkSize);
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, sql_utils_1.getQualifiedTableName)(this.schema);
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, sql_utils_1.getQualifiedTableName)(this.schema);
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
- if (returningClause) {
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 schema = table._getSchema();
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
- // Build RETURNING clause
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
- if (returningClause) {
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 sql = `WITH "__mutation__" AS (
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
  /**