@vantreeseba/drizzle-graphql 15.1.5 → 15.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1375,7 +1375,10 @@ can count or summarise related rows without fetching them:
1375
1375
  All parents in a selection are aggregated with a single
1376
1376
  `SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
1377
1377
  users is one extra query, not one per user. Differently-aliased selections with different
1378
- `where` arguments are batched separately.
1378
+ `where` arguments are batched separately. A batch whose parent keys would bind more than
1379
+ 10,000 parameters (10,000 parents on a single-column key, 5,000 on a two-column one) is split
1380
+ into several such queries, which keeps it within SQLite's 32,766- and Postgres/MySQL's
1381
+ 65,535-parameter limits; the batched relation fields split the same way.
1379
1382
 
1380
1383
  ## Distinct
1381
1384
 
@@ -1604,8 +1607,9 @@ mutation {
1604
1607
  out the rest of the row. Listing an unsupplied column in `update` is an error rather than
1605
1608
  a silent no-op
1606
1609
  - That holds per row: a batch whose rows supply different columns is split into one
1607
- statement per run of consecutive rows that write the same columns, run in one transaction,
1608
- so a row that left a column out keeps it. The rows come back in the order they were sent
1610
+ statement per run of consecutive rows that write the same columns, run in one transaction
1611
+ (on a synchronous SQLite driver such as better-sqlite3 too), so a row that left a column out
1612
+ keeps it. The rows come back in the order they were sent
1609
1613
  - `target` must match one of the table's unique constraints exactly; anything else is
1610
1614
  rejected with the list of valid targets, instead of the database's opaque "no unique or
1611
1615
  exclusion constraint matching" error
@@ -2486,7 +2490,15 @@ Generated schemas resolve nested relations without N+1 query explosions:
2486
2490
  `ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
2487
2491
  parent. A relation joined on several columns (`from: [a, b], to: [x, y]`) is batched,
2488
2492
  grouped and partitioned by all of them, matching the parents with
2489
- `(a = … AND b = …) OR …` instead of `IN (…)`.
2493
+ `(a = … AND b = …) OR …` instead of `IN (…)`, nested as a balanced tree so a batch of
2494
+ thousands of parents stays within SQLite's expression-depth limit. A batch over 10,000 key
2495
+ parameters runs as several queries, each parent read whole by one of them. The parent's join values are bound as the
2496
+ column holds them, not in their GraphQL output form, so a relation joined on a timestamp,
2497
+ `date`, `bigint` or decimal column works on this path and in `<relation>Aggregate` too. A
2498
+ parent row this library remapped keeps its original values for that; a parent your own
2499
+ resolver built in GraphQL form (an ISO string for a timestamp, say) has them mapped back the
2500
+ way an input value would be, which cuts a `'string'`-mode microsecond timestamp to
2501
+ milliseconds — return the row as drizzle read it to keep the full precision.
2490
2502
 
2491
2503
  ```Typescript
2492
2504
  // entities.fieldResolvers is keyed by table name, then relation name
package/dist/index.cjs CHANGED
@@ -1259,6 +1259,39 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1259
1259
  }
1260
1260
  return value;
1261
1261
  };
1262
+ /**
1263
+ * Where a remapped row keeps the values its columns held before remapping, by property name —
1264
+ * only for the columns remapping changed. The output form is not always reversible: a
1265
+ * microsecond timestamp read in `'string'` mode leaves as an ISO string the `DateTime` input
1266
+ * side would cut to milliseconds. A relation field correlating on the row's join columns reads
1267
+ * them from here (see `parentJoinValues`). Non-enumerable, so the row's own keys, JSON form and
1268
+ * equality are unchanged.
1269
+ */
1270
+ const RAW_COLUMN_VALUES = Symbol("drizzle-graphql.rawColumnValues");
1271
+ /**
1272
+ * The value column `key` held in `row` before remapping: `{ found: true, value }` when remapping
1273
+ * changed it, `{ found: false }` when it did not (or the row was never remapped), in which case
1274
+ * `row[key]` is still the column's own value.
1275
+ */
1276
+ const rawColumnValue = (row, key) => {
1277
+ const raw = row[RAW_COLUMN_VALUES];
1278
+ return raw && key in raw ? {
1279
+ found: true,
1280
+ value: raw[key]
1281
+ } : { found: false };
1282
+ };
1283
+ const keepRawColumnValue = (row, key, value) => {
1284
+ let raw = row[RAW_COLUMN_VALUES];
1285
+ if (!raw) {
1286
+ raw = Object.create(null);
1287
+ Object.defineProperty(row, RAW_COLUMN_VALUES, {
1288
+ value: raw,
1289
+ enumerable: false,
1290
+ configurable: true
1291
+ });
1292
+ }
1293
+ if (!(key in raw)) raw[key] = value;
1294
+ };
1262
1295
  const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap) => {
1263
1296
  const columns = (0, drizzle_orm.getColumns)(table);
1264
1297
  for (const [key, value] of Object.entries(queryOutput)) {
@@ -1276,7 +1309,9 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
1276
1309
  delete queryOutput[key];
1277
1310
  continue;
1278
1311
  }
1279
- queryOutput[key] = remapToGraphQLCore(key, value, tableName, column, relationMap);
1312
+ const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
1313
+ if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
1314
+ queryOutput[key] = remapped;
1280
1315
  }
1281
1316
  return queryOutput;
1282
1317
  };
@@ -1855,17 +1890,104 @@ const extractRelationJoinColumnPairs = (relEntry, parentTable, targetTable) => {
1855
1890
  }
1856
1891
  return pairs;
1857
1892
  };
1893
+ /** A decimal written as text, in a form `canonicalDecimal` can normalise. */
1894
+ const DECIMAL_TEXT = /^([+-]?)(\d*)(?:\.(\d*))?$/;
1895
+ /**
1896
+ * Decimal text without the zeros that do not change its value: `1.500` and `01.5` are both
1897
+ * `1.5`, as SQL compares them. Anything else (an exponent, `NaN`) is returned unchanged.
1898
+ */
1899
+ const canonicalDecimal = (value) => {
1900
+ const match = DECIMAL_TEXT.exec(value);
1901
+ if (!match || !match[2] && !match[3]) return value;
1902
+ const whole = match[2].replace(/^0+(?=\d)/, "") || "0";
1903
+ const fraction = (match[3] ?? "").replace(/0+$/, "");
1904
+ const digits = fraction ? `${whole}.${fraction}` : whole;
1905
+ return match[1] === "-" && digits !== "0" ? `-${digits}` : digits;
1906
+ };
1907
+ /** An ISO instant without trailing zeros in its fraction: `…:00.100Z` and `…:00.1Z` agree. */
1908
+ const trimIsoFraction = (iso) => iso.replace(/(\.\d*?)0+Z$/, "$1Z").replace(/\.Z$/, "Z");
1909
+ /**
1910
+ * One join value in a form both sides of a relation produce for the same SQL value. The parent's
1911
+ * key and a related row's can arrive in different JS forms — a `Date` and the text of the same
1912
+ * instant, `1.50` and `1.500` from decimals of different scale — and `String()` would tell them
1913
+ * apart (it even drops a `Date`'s milliseconds).
1914
+ */
1915
+ const joinValueKey = (value, column) => {
1916
+ if (value instanceof Date) {
1917
+ if (Number.isNaN(value.getTime())) return String(value);
1918
+ if (column && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value.toISOString();
1919
+ return trimIsoFraction(value.toISOString());
1920
+ }
1921
+ if (typeof value === "bigint") return value.toString();
1922
+ if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
1923
+ if (typeof value === "string" && column) {
1924
+ if (isMySqlDateTimeStringColumn(column)) return trimIsoFraction(mySqlDateTimeTextToIso(value));
1925
+ if (isPgTimestampStringColumn(column)) return trimIsoFraction(pgTimestampTextToIso(value));
1926
+ if ((column.dataType ?? "") === "string numeric") return canonicalDecimal(value);
1927
+ }
1928
+ return String(value);
1929
+ };
1858
1930
  /**
1859
1931
  * A string identifying one combination of join-column values, for grouping a batch's rows by
1860
- * parent. Values are compared by their string form, as a single-column key always has been.
1932
+ * parent. `columns` are the columns the values were read from, in the same order: the parent's
1933
+ * join columns for a parent key, the target's (or the junction's) for a related row. Values the
1934
+ * database compares equal give the same key — see {@link joinValueKey}.
1935
+ */
1936
+ const joinKeyOf = (values, columns) => values.length === 1 ? joinValueKey(values[0], columns?.[0]) : JSON.stringify(values.map((value, i) => joinValueKey(value, columns?.[i])));
1937
+ /**
1938
+ * The parent row's values for the relation's join columns, as the database holds them. The row
1939
+ * a relation field resolves under has been remapped to its GraphQL form — a `Date` is an ISO
1940
+ * string by then, a `bigint` a decimal string — which can neither be bound back through every
1941
+ * column's encoder (SQLite's integer timestamp calls `getTime()` on it) nor be compared with the
1942
+ * related rows' decoded values. The values from before remapping are used when the row kept
1943
+ * them; otherwise the output form is mapped back the way an input value would be.
1944
+ */
1945
+ const parentJoinValues = (parent, pairs) => pairs.map(({ localColPropName, localCol }) => {
1946
+ const raw = rawColumnValue(parent, localColPropName);
1947
+ if (raw.found) return raw.value;
1948
+ const value = parent[localColPropName];
1949
+ if (value == null) return value;
1950
+ try {
1951
+ return remapFromGraphQLCore(value, localCol, localColPropName);
1952
+ } catch {
1953
+ return value;
1954
+ }
1955
+ });
1956
+ /**
1957
+ * At most this many bound parameters for the parent keys of one batched relation query. SQLite
1958
+ * accepts 32,766 per statement, Postgres and MySQL 65,535; the rest is left to the request's own
1959
+ * filters. A batch with more parents runs as several queries.
1960
+ */
1961
+ const MAX_PARENT_KEY_PARAMS = 1e4;
1962
+ /**
1963
+ * The parent keys of one batch split into the groups each query reads, so that no query binds
1964
+ * more than {@link MAX_PARENT_KEY_PARAMS} key values. Every parent lands in exactly one group, so
1965
+ * a per-parent result — its rows, its window, its count — is the same whichever query made it.
1861
1966
  */
1862
- const joinKeyOf = (values) => values.length === 1 ? String(values[0]) : JSON.stringify(values.map((value) => String(value)));
1967
+ const parentKeyChunks = (keys, width) => {
1968
+ const size = Math.max(1, Math.floor(MAX_PARENT_KEY_PARAMS / Math.max(1, width)));
1969
+ const chunks = [];
1970
+ for (let i = 0; i < keys.length; i += size) chunks.push(keys.slice(i, i + size));
1971
+ return chunks;
1972
+ };
1973
+ /**
1974
+ * `or` over `conditions` as a balanced tree. A flat `a OR b OR c …` is parsed left-deep, and
1975
+ * SQLite refuses an expression more than 1,000 levels deep; balanced, a batch of thousands of
1976
+ * parents stays a few levels deep.
1977
+ */
1978
+ const balancedOr = (conditions) => {
1979
+ if (conditions.length <= 2) return (0, drizzle_orm.or)(...conditions);
1980
+ const middle = Math.ceil(conditions.length / 2);
1981
+ return (0, drizzle_orm.or)(balancedOr(conditions.slice(0, middle)), balancedOr(conditions.slice(middle)));
1982
+ };
1863
1983
  /**
1864
1984
  * The rows whose `cols` hold one of `keys`: `col IN (…)` for a single join column, and an `OR`
1865
1985
  * of per-parent equalities for several — `(a = 1 AND b = 1) OR (a = 1 AND b = 2)` — which
1866
- * every dialect accepts, unlike a row-value `IN` list.
1986
+ * every dialect accepts, unlike a row-value `IN` list. The `OR` is nested as a balanced tree.
1987
+ * Callers split a large batch with {@link parentKeyChunks} first, which keeps either form within
1988
+ * the dialects' parameter limits.
1867
1989
  */
1868
- const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)(...cols.map((col, i) => (0, drizzle_orm.eq)(col, key[i])))));
1990
+ const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : balancedOr(keys.map((key) => (0, drizzle_orm.and)(...cols.map((col, i) => (0, drizzle_orm.eq)(col, key[i])))));
1869
1991
  /**
1870
1992
  * The junction table and its two columns for a `.through()` (many-to-many) relation:
1871
1993
  * `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
@@ -4961,7 +5083,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4961
5083
  if (!parentTable || !targetTable) return;
4962
5084
  const joinPairs = extractRelationJoinColumnPairs(relEntry, parentTable, targetTable);
4963
5085
  if (!joinPairs) return;
4964
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
5086
+ const localCols = joinPairs.map((pair) => pair.localCol);
4965
5087
  const foreignCol = joinPairs[0].foreignCol;
4966
5088
  const junction = extractJunctionColumns(relEntry);
4967
5089
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
@@ -5007,7 +5129,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5007
5129
  };
5008
5130
  const resolve = async (parent, args, context, info) => {
5009
5131
  if (parent[relationName] !== void 0) return parent[relationName];
5010
- const localValues = localColPropNames.map((name) => parent[name]);
5132
+ const localValues = parentJoinValues(parent, joinPairs);
5011
5133
  if (localValues.some((value) => value == null)) return isOne ? null : [];
5012
5134
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
5013
5135
  const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
@@ -5051,55 +5173,60 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5051
5173
  if (junction === null) throw drizzleError(`Relation '${tableName}.${relationName}' goes through a junction with a composite key, which can only be loaded together with its parent. Leave it to eager loading (eagerLoadRelations).`, { code: "DRIZZLE_UNSUPPORTED_RELATION" });
5052
5174
  return getOrCreateLoader(context, loaderKey, async (parentKeys) => {
5053
5175
  const executor = resolveExecutor(db, context);
5054
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
5176
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
5055
5177
  const scope = resolveScope(policies, context, filterCtx);
5056
- let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5057
- if (distinct) {
5058
- const keys = await selectDistinctKeys({
5059
- db: executor,
5060
- table: targetTable,
5061
- tableName: targetTableName,
5062
- distinct,
5063
- pkNames: targetPkNames,
5064
- where: whereCondition,
5065
- orderBy: orderByArg,
5066
- partitionBy: groupCols,
5067
- junction: junction ? {
5068
- table: junction.table,
5069
- on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
5070
- parentKey: junction.sourceCol
5071
- } : void 0
5072
- });
5073
- if (!keys.length) return parentKeys.map(() => isOne ? null : []);
5074
- whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5075
- }
5076
- const selection = junction ? {
5077
- ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
5078
- [PARENT_KEY]: junction.sourceCol
5079
- } : columns;
5080
- let rows;
5081
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, groupCols, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
5082
- else {
5083
- let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5084
- const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5085
- if (orderExprs.length) q = q.orderBy(...orderExprs);
5086
- rows = await q;
5087
- }
5088
- if (cursorEntries) attachRowCursors(rows, cursorEntries);
5089
- const keyOf = (row) => {
5090
- const key = joinKeyOf(groupKeys.map((name) => row[name]));
5091
- if (junction) delete row[PARENT_KEY];
5092
- return key;
5093
- };
5094
- if (isOne) {
5095
- const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5178
+ const loadChunk = async (chunkKeys) => {
5179
+ let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, chunkKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5180
+ if (distinct) {
5181
+ const keys = await selectDistinctKeys({
5182
+ db: executor,
5183
+ table: targetTable,
5184
+ tableName: targetTableName,
5185
+ distinct,
5186
+ pkNames: targetPkNames,
5187
+ where: whereCondition,
5188
+ orderBy: orderByArg,
5189
+ partitionBy: groupCols,
5190
+ junction: junction ? {
5191
+ table: junction.table,
5192
+ on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
5193
+ parentKey: junction.sourceCol
5194
+ } : void 0
5195
+ });
5196
+ if (!keys.length) return /* @__PURE__ */ new Map();
5197
+ whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5198
+ }
5199
+ const selection = junction ? {
5200
+ ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
5201
+ [PARENT_KEY]: junction.sourceCol
5202
+ } : columns;
5203
+ let rows;
5204
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, groupCols, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
5205
+ else {
5206
+ let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5207
+ const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5208
+ if (orderExprs.length) q = q.orderBy(...orderExprs);
5209
+ rows = await q;
5210
+ }
5211
+ if (cursorEntries) attachRowCursors(rows, cursorEntries);
5212
+ const keyOf = (row) => {
5213
+ const key = joinKeyOf(groupKeys.map((name) => row[name]), groupCols);
5214
+ if (junction) delete row[PARENT_KEY];
5215
+ return key;
5216
+ };
5217
+ if (isOne) {
5218
+ const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5219
+ remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5220
+ return byKey;
5221
+ }
5222
+ const grouped = new Map(chunkKeys.map((key) => [joinKeyOf(key, localCols), []]));
5223
+ for (const row of rows) grouped.get(keyOf(row))?.push(row);
5096
5224
  remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5097
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? null);
5098
- }
5099
- const grouped = new Map(uniqueKeys.map((key) => [joinKeyOf(key), []]));
5100
- for (const row of rows) grouped.get(keyOf(row))?.push(row);
5101
- remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5102
- return parentKeys.map((key) => grouped.get(joinKeyOf(key)) ?? []);
5225
+ return grouped;
5226
+ };
5227
+ const results = /* @__PURE__ */ new Map();
5228
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) for (const [key, value] of await loadChunk(chunk)) results.set(key, value);
5229
+ return parentKeys.map((key) => results.get(joinKeyOf(key, localCols)) ?? (isOne ? null : []));
5103
5230
  }).load(localValues);
5104
5231
  };
5105
5232
  return async (parent, args, context, info) => {
@@ -6185,7 +6312,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6185
6312
  }
6186
6313
  return;
6187
6314
  }
6188
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
6315
+ const localCols = joinPairs.map((pair) => pair.localCol);
6189
6316
  const foreignCol = joinPairs[0].foreignCol;
6190
6317
  const junction = extractJunctionColumns(relEntry);
6191
6318
  if (junction === null) return;
@@ -6215,23 +6342,26 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6215
6342
  };
6216
6343
  });
6217
6344
  if (!Object.keys(request.selection).length) return {};
6218
- const localValues = localColPropNames.map((name) => parent[name]);
6345
+ const localValues = parentJoinValues(parent, joinPairs);
6219
6346
  if (localValues.some((value) => value == null)) return assembleAggregateRow({}, request, target);
6220
6347
  const whereArg = args?.where;
6221
6348
  const deleted = args?.deleted;
6222
6349
  const loader = getOrCreateLoader(context, loaderKey, async (parentKeys) => {
6223
6350
  const executor = resolveExecutor(db, context);
6224
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
6351
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
6225
6352
  const scope = resolveScope(policies, context, filterCtx);
6226
- const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6227
6353
  const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
6228
- const selected = executor.select({
6229
- ...groupSelection,
6230
- ...request.selection
6231
- }).from(targetTable);
6232
- const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6233
- const byKey = new Map(rows.map((row) => [joinKeyOf(groupAliases.map((alias) => row[alias])), row]));
6234
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? {});
6354
+ const byKey = /* @__PURE__ */ new Map();
6355
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
6356
+ const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, chunk), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6357
+ const selected = executor.select({
6358
+ ...groupSelection,
6359
+ ...request.selection
6360
+ }).from(targetTable);
6361
+ const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6362
+ for (const row of rows) byKey.set(joinKeyOf(groupAliases.map((alias) => row[alias]), groupCols), row);
6363
+ }
6364
+ return parentKeys.map((key) => byKey.get(joinKeyOf(key, localCols)) ?? {});
6235
6365
  });
6236
6366
  return assembleAggregateRow(await loader.load(localValues), request, target);
6237
6367
  } catch (e) {
@@ -8318,7 +8448,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8318
8448
  serverOwned: Object.keys(contextColumns ?? {}),
8319
8449
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8320
8450
  });
8321
- const runBatch = async (executor, { values: rows, plan }) => {
8451
+ const batchQuery = (executor, { values: rows, plan }) => {
8322
8452
  let query = executor.insert(table).values(rows).returning(returning);
8323
8453
  const setWhere = withScope(scope, tableName, table, plan.setWhere);
8324
8454
  query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
@@ -8330,12 +8460,22 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8330
8460
  set: plan.set,
8331
8461
  setWhere
8332
8462
  });
8333
- return await query;
8463
+ return query;
8334
8464
  };
8335
- if (batches.length === 1) return runBatch(target, batches[0]);
8336
- return target.transaction(async (tx) => {
8337
- const rows = [];
8338
- for (const batch of batches) rows.push(...await runBatch(tx, batch));
8465
+ if (batches.length === 1) return await batchQuery(target, batches[0]);
8466
+ return target.transaction((tx) => {
8467
+ const execute = (batch) => {
8468
+ const query = batchQuery(tx, batch);
8469
+ return typeof query.all === "function" ? query.all() : query;
8470
+ };
8471
+ const first = execute(batches[0]);
8472
+ if (typeof first?.then === "function") return (async () => {
8473
+ const rows = [...await first];
8474
+ for (const batch of batches.slice(1)) rows.push(...await execute(batch));
8475
+ return rows;
8476
+ })();
8477
+ const rows = [...first];
8478
+ for (const batch of batches.slice(1)) rows.push(...execute(batch));
8339
8479
  return rows;
8340
8480
  });
8341
8481
  };