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.
@@ -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,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
- if (returningClause) {
2114
- sql += ` RETURNING ${returningClause.sql}`;
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 result = executor
2117
- ? await executor.query(sql, params)
2118
- : await client.query(sql, params);
2119
- if (!returningClause) {
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
- return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
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
- * Execute a single upsert batch
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
- async upsertBulkSingle(values, primaryKeys, updateColumns, updateColumnFilter, overridingSystemValue, targetWhere, setWhere, returning) {
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 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
- }
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
- // Build RETURNING clause
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
- 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);
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 sql = `WITH "__mutation__" AS (
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
  /**