turbine-orm 0.90.2 → 0.91.0-next.32b6789

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.
Files changed (45) hide show
  1. package/README.md +23 -24
  2. package/dist/cjs/dialect.d.ts +26 -1
  3. package/dist/cjs/dialect.js +4 -0
  4. package/dist/cjs/index.d.ts +2 -2
  5. package/dist/cjs/mssql.js +2 -0
  6. package/dist/cjs/mysql.js +3 -0
  7. package/dist/cjs/powdb.js +2 -0
  8. package/dist/cjs/powql.d.ts +5 -2
  9. package/dist/cjs/powql.js +49 -28
  10. package/dist/cjs/prisma-compat.d.ts +2 -2
  11. package/dist/cjs/prisma-compat.js +8 -5
  12. package/dist/cjs/query/aggregates.js +74 -4
  13. package/dist/cjs/query/builder.d.ts +20 -7
  14. package/dist/cjs/query/builder.js +31 -22
  15. package/dist/cjs/query/expr.d.ts +47 -0
  16. package/dist/cjs/query/expr.js +129 -3
  17. package/dist/cjs/query/index.d.ts +1 -1
  18. package/dist/cjs/query/option-surface.js +3 -0
  19. package/dist/cjs/query/types.d.ts +107 -7
  20. package/dist/cjs/query/writes.d.ts +14 -2
  21. package/dist/cjs/query/writes.js +65 -18
  22. package/dist/cjs/sqlite.js +6 -0
  23. package/dist/dialect.d.ts +26 -1
  24. package/dist/dialect.js +4 -0
  25. package/dist/index.d.ts +2 -2
  26. package/dist/mssql.js +2 -0
  27. package/dist/mysql.js +3 -0
  28. package/dist/powdb.js +2 -0
  29. package/dist/powql.d.ts +5 -2
  30. package/dist/powql.js +49 -28
  31. package/dist/prisma-compat.d.ts +2 -2
  32. package/dist/prisma-compat.js +8 -5
  33. package/dist/query/aggregates.js +74 -4
  34. package/dist/query/builder.d.ts +20 -7
  35. package/dist/query/builder.js +31 -22
  36. package/dist/query/expr.d.ts +47 -0
  37. package/dist/query/expr.js +128 -3
  38. package/dist/query/index.d.ts +1 -1
  39. package/dist/query/option-surface.js +3 -0
  40. package/dist/query/types.d.ts +107 -7
  41. package/dist/query/writes.d.ts +14 -2
  42. package/dist/query/writes.js +63 -18
  43. package/dist/sqlite.js +6 -0
  44. package/package.json +1 -1
  45. package/skills/turbine-orm/SKILL.md +8 -0
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { UnsupportedFeatureError, ValidationError } from '../errors.js';
12
12
  import { snakeToCamel } from '../schema.js';
13
+ import { compileReadExpr } from './expr.js';
13
14
  import { assertEqualityOperand, dedupeColumnList, isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, setOwn, } from './filters.js';
14
15
  import { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
15
16
  import { isTemporalInfinity, ownLookup, parseDbDate, resolveColumnName, suggestKey, warnRedundantSortTerm, } from './utils.js';
@@ -281,7 +282,7 @@ export function buildGroupBy(qi, args) {
281
282
  buildAggregates('_avg', 'AVG', args._avg);
282
283
  buildAggregates('_min', 'MIN', args._min);
283
284
  buildAggregates('_max', 'MAX', args._max);
284
- const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey);
285
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey, (key) => key === '_count' || usedResultKeys.has(key));
285
286
  selectExprs.push(...percentiles.map((plan) => plan.expr));
286
287
  let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
287
288
  // HAVING, filter whole groups by their aggregate values.
@@ -1026,7 +1027,7 @@ const PERCENTILE_HINT = {
1026
1027
  mssql: 'Percentile aggregates are not available on SQL Server in this release: PERCENTILE_CONT is a window ' +
1027
1028
  'function there, not an aggregate.',
1028
1029
  };
1029
- const PERCENTILE_SPEC_KEYS = ['p', 'method'];
1030
+ const PERCENTILE_SPEC_KEYS = ['p', 'method', 'of'];
1030
1031
  const PERCENTILE_METHODS = ['cont', 'disc'];
1031
1032
  const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is not supported yet.';
1032
1033
  /**
@@ -1041,8 +1042,14 @@ const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is
1041
1042
  * form), a JSON-path target, and two keys naming one column. Both methods need
1042
1043
  * `includePii: UNSAFE` on a PII column: `disc` returns a stored cell, and so
1043
1044
  * does `cont` at p = 0 or 1 or over a one-row group.
1045
+ *
1046
+ * A key with `of` names an expression's result (see `PercentileExprSpec`): it
1047
+ * must not name a field, the expression compiles through `compileReadExpr`,
1048
+ * it is ordered as `float8`, and its SQL alias is `_percentile_of_<n>`, so the
1049
+ * caller's name never reaches the SQL. Every PII column it reads as a value
1050
+ * needs `includePii: UNSAFE`, as a column percentile does.
1044
1051
  */
1045
- function planPercentiles(qi, spec, params, includePii, operation, claimResultKey) {
1052
+ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey, resultKeyTaken) {
1046
1053
  if (spec === undefined)
1047
1054
  return [];
1048
1055
  const where = `${operation} _percentile on table "${qi.table}"`;
@@ -1058,6 +1065,16 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1058
1065
  const meta = qi.schema.tables[qi.table];
1059
1066
  const plans = [];
1060
1067
  const aliases = new Set();
1068
+ // Column aliases are `_percentile_<column>`; an expression's is the first
1069
+ // free `_percentile_of_<n>`, chosen after every column alias is known so a
1070
+ // column named `of_0` keeps its own.
1071
+ const columnAliases = new Set();
1072
+ for (const [key] of entries) {
1073
+ const column = resolveColumnName(qi.tableMeta, key);
1074
+ if (column !== undefined)
1075
+ columnAliases.add(`_percentile_${column}`);
1076
+ }
1077
+ let exprCount = 0;
1061
1078
  for (const [key, entry] of entries) {
1062
1079
  const label = `${where}, field "${key}"`;
1063
1080
  if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
@@ -1072,7 +1089,7 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1072
1089
  throw new ValidationError(`${label}: unknown key "${k}".${hint ? ` Did you mean "${hint}"?` : ''} Expected { p, method? }.`);
1073
1090
  }
1074
1091
  }
1075
- const { p, method = 'cont' } = entry;
1092
+ const { p, method = 'cont', of } = entry;
1076
1093
  if (!PERCENTILE_METHODS.includes(method)) {
1077
1094
  throw new ValidationError(`${label}: method must be 'cont' or 'disc', received ${describeValue(method)}.`);
1078
1095
  }
@@ -1085,6 +1102,10 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1085
1102
  else {
1086
1103
  assertFraction(p, label);
1087
1104
  }
1105
+ if (of !== undefined) {
1106
+ plans.push(planExpressionPercentile(qi, key, of, p, method, params, includePii, operation, label, aliases, columnAliases, exprCount++, claimResultKey, resultKeyTaken));
1107
+ continue;
1108
+ }
1088
1109
  const col = qi.toColumn(key);
1089
1110
  if (method === 'cont' && qi.tableMeta.dateColumns.has(col)) {
1090
1111
  throw new ValidationError(`${label}: method 'cont' interpolates and needs a numeric or interval column, and "${col}" is a ` +
@@ -1114,6 +1135,51 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1114
1135
  }
1115
1136
  return plans;
1116
1137
  }
1138
+ /** One `_percentile` entry over an expression (`of`); see {@link planPercentiles}. */
1139
+ function planExpressionPercentile(qi, key, of, p, method, params, includePii, operation, label, aliases, columnAliases, index, claimResultKey, resultKeyTaken) {
1140
+ const column = resolveColumnName(qi.tableMeta, key);
1141
+ if (column !== undefined) {
1142
+ throw new ValidationError(`${label}: "${key}" is a field of the table, and \`of\` names an expression. Give the expression a name ` +
1143
+ `that is not a field (the name is only its result's key), or drop \`of\` for the percentile of "${key}".`);
1144
+ }
1145
+ const compiled = compileReadExpr({
1146
+ qi,
1147
+ name: key,
1148
+ operation: `${operation} _percentile`,
1149
+ bind: (value, cls) => {
1150
+ params.push(value);
1151
+ return `${qi.p(params.length)}::${cls === 'int' ? 'int8' : 'numeric'}`;
1152
+ },
1153
+ predicate: (when) => {
1154
+ const sql = whereMod.buildWhereClause(qi, when, params);
1155
+ if (sql === null)
1156
+ throw new ValidationError(`${label}: an expr.case() arm's when compiles to no predicate.`);
1157
+ return sql;
1158
+ },
1159
+ }, of);
1160
+ const meta = qi.schema.tables[qi.table];
1161
+ for (const col of compiled.values) {
1162
+ const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
1163
+ assertAggregatePiiOptIn(qi.table, meta, field, col, `${operation} _percentile "${key}"`, includePii);
1164
+ }
1165
+ // The caller's key never names the alias, so it skips every name already
1166
+ // taken, a groupBy key included, rather than collide with one.
1167
+ let n = index;
1168
+ const taken = (alias) => aliases.has(alias) || columnAliases.has(alias) || resultKeyTaken?.(alias) === true;
1169
+ while (taken(`_percentile_of_${n}`))
1170
+ n++;
1171
+ const alias = `_percentile_of_${n}`;
1172
+ aliases.add(alias);
1173
+ claimResultKey?.(alias, `_percentile "${key}"`);
1174
+ params.push(Array.isArray(p) ? [...p] : p);
1175
+ const fractions = `${qi.p(params.length)}::float8${Array.isArray(p) ? '[]' : ''}`;
1176
+ return {
1177
+ expr: `percentile_${method}(${fractions}) WITHIN GROUP (ORDER BY (${compiled.sql})::float8) AS ${qi.q(alias)}`,
1178
+ alias,
1179
+ field: key,
1180
+ col: null,
1181
+ };
1182
+ }
1117
1183
  function assertFraction(value, label) {
1118
1184
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
1119
1185
  return;
@@ -1155,6 +1221,10 @@ function readPercentiles(qi, plans, row) {
1155
1221
  function percentileElement(qi, col, value) {
1156
1222
  if (value === null || value === undefined)
1157
1223
  return null;
1224
+ // An expression is ordered as float8, which the driver reads as a number
1225
+ // (a supplied pool's own parser may hand back text).
1226
+ if (col === null)
1227
+ return typeof value === 'string' ? Number(value) : value;
1158
1228
  if (typeof value === 'string' && isInt8Type(whereMod.getColumnPgType(qi, col))) {
1159
1229
  const n = Number(value);
1160
1230
  return Number.isSafeInteger(n) ? n : value;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { PgCompatPool } from '../pg-types.js';
14
14
  import type { SchemaMetadata } from '../schema.js';
15
- import type { AggregateArgs, AggregateResultOf, CountArgs, CreateArgs, CreateManyArgs, DefaultInputTypes, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TableInputTypes, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
15
+ import type { AggregateArgs, AggregateResultOf, CountArgs, CreateArgs, CreateManyArgs, CreateManyResult, DefaultInputTypes, DeleteArgs, DeleteManyArgs, FieldPercentiles, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, PercentileKeysChecked, QueryResult, TableInputTypes, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
16
16
  /**
17
17
  * Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
18
18
  *
@@ -1180,7 +1180,12 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
1180
1180
  * writes.ts because relations.ts imports writes.ts.
1181
1181
  */
1182
1182
  private resolveWriteProjection;
1183
- buildCreateMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateManyArgs<T, M['createMany'], S, O>): DeferredQuery<FieldResult<T, S, O>[]>;
1183
+ buildCreateMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined, C extends boolean | undefined = undefined>(args: CreateManyArgs<T, M['createMany'], S, O, C>): DeferredQuery<CreateManyResult<FieldResult<T, S, O>, C>>;
1184
+ /**
1185
+ * `createMany`'s projection: the caller's `select` / `omit`, or
1186
+ * {@link writesMod.COUNT_ONLY} for `returning: false`, which takes neither.
1187
+ */
1188
+ private createManyProjection;
1184
1189
  buildUpdate<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O, M['update']>): DeferredQuery<FieldResult<T, S, O>>;
1185
1190
  buildDelete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
1186
1191
  buildUpsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O, M['whereUnique'], M['createMany'], M['updateMany']>): DeferredQuery<FieldResult<T, S, O>>;
@@ -1416,7 +1421,7 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
1416
1421
  findUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O, M['whereUnique']>): Promise<QueryResult<T, R, W, S, O>>;
1417
1422
  buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O, M['whereUnique']>): DeferredQuery<QueryResult<T, R, W, S, O>>;
1418
1423
  create<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateArgs<T, R, S, O, M['create']>): Promise<FieldResult<T, S, O>>;
1419
- createMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateManyArgs<T, M['createMany'], S, O>): Promise<FieldResult<T, S, O>[]>;
1424
+ createMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined, C extends boolean | undefined = undefined>(args: CreateManyArgs<T, M['createMany'], S, O, C>): Promise<CreateManyResult<FieldResult<T, S, O>, C>>;
1420
1425
  update<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O, M['update']>): Promise<FieldResult<T, S, O>>;
1421
1426
  private nestedCreate;
1422
1427
  private nestedUpdate;
@@ -1477,10 +1482,18 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
1477
1482
  * {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
1478
1483
  * that cannot be typed, so those columns are not projected onto the row type.
1479
1484
  */
1480
- groupBy<A extends GroupByArgs<T, R>>(args: A): Promise<GroupByResult<T, A>[]>;
1481
- buildGroupBy<A extends GroupByArgs<T, R>>(args: A): DeferredQuery<GroupByResult<T, A>[]>;
1482
- buildAggregate<A extends AggregateArgs<T, R>>(args: A): DeferredQuery<AggregateResultOf<T, A>>;
1483
- aggregate<A extends AggregateArgs<T, R>>(args: A): Promise<AggregateResultOf<T, A>>;
1485
+ groupBy<A extends GroupByArgs<T, R>>(args: A & PercentileKeysChecked<T, A>): Promise<GroupByResult<T, A>[]>;
1486
+ /** Args of a generic type: field percentiles only (see {@link FieldPercentiles}). */
1487
+ groupBy<A extends Omit<GroupByArgs<T, R>, '_percentile'> & FieldPercentiles<T>>(args: A): Promise<GroupByResult<T, A>[]>;
1488
+ buildGroupBy<A extends GroupByArgs<T, R>>(args: A & PercentileKeysChecked<T, A>): DeferredQuery<GroupByResult<T, A>[]>;
1489
+ /** Args of a generic type: field percentiles only (see {@link FieldPercentiles}). */
1490
+ buildGroupBy<A extends Omit<GroupByArgs<T, R>, '_percentile'> & FieldPercentiles<T>>(args: A): DeferredQuery<GroupByResult<T, A>[]>;
1491
+ buildAggregate<A extends AggregateArgs<T, R>>(args: A & PercentileKeysChecked<T, A>): DeferredQuery<AggregateResultOf<T, A>>;
1492
+ /** Args of a generic type: field percentiles only (see {@link FieldPercentiles}). */
1493
+ buildAggregate<A extends Omit<AggregateArgs<T, R>, '_percentile'> & FieldPercentiles<T>>(args: A): DeferredQuery<AggregateResultOf<T, A>>;
1494
+ aggregate<A extends AggregateArgs<T, R>>(args: A & PercentileKeysChecked<T, A>): Promise<AggregateResultOf<T, A>>;
1495
+ /** Args of a generic type: field percentiles only (see {@link FieldPercentiles}). */
1496
+ aggregate<A extends Omit<AggregateArgs<T, R>, '_percentile'> & FieldPercentiles<T>>(args: A): Promise<AggregateResultOf<T, A>>;
1484
1497
  private resolveColumns;
1485
1498
  withFingerprint(withClause: WithClause | undefined, table?: string, depth?: number): string;
1486
1499
  private collectWithParams;
@@ -3010,7 +3010,24 @@ export class QueryInterface {
3010
3010
  }
3011
3011
  buildCreateMany(args) {
3012
3012
  args = this.normalizeArgs(args);
3013
- return writesMod.buildCreateMany(this.ctx, args, 'refuse', this.resolveWriteProjection(args));
3013
+ return writesMod.buildCreateMany(this.ctx, args, 'refuse', this.createManyProjection(args));
3014
+ }
3015
+ /**
3016
+ * `createMany`'s projection: the caller's `select` / `omit`, or
3017
+ * {@link writesMod.COUNT_ONLY} for `returning: false`, which takes neither.
3018
+ */
3019
+ createManyProjection(args) {
3020
+ const { returning } = args;
3021
+ if (returning === undefined || returning === true)
3022
+ return this.resolveWriteProjection(args);
3023
+ if (returning !== false) {
3024
+ throw new ValidationError(`createMany on "${this.table}": returning must be true or false, received ${typeof returning}.`);
3025
+ }
3026
+ if (args.select !== undefined || args.omit !== undefined) {
3027
+ throw new ValidationError(`createMany on "${this.table}": returning: false returns { count } and no rows, so it takes no ` +
3028
+ `\`${args.select !== undefined ? 'select' : 'omit'}\`. Drop one of them.`);
3029
+ }
3030
+ return writesMod.COUNT_ONLY;
3014
3031
  }
3015
3032
  buildUpdate(args) {
3016
3033
  args = this.normalizeArgs(args);
@@ -4403,13 +4420,17 @@ export class QueryInterface {
4403
4420
  async createMany(args) {
4404
4421
  args = this.normalizeArgs(args);
4405
4422
  return this.executeWithMiddleware('createMany', args, async () => {
4406
- const projection = this.resolveWriteProjection(args);
4423
+ const projection = this.createManyProjection(args);
4424
+ const countOnly = writesMod.isCountOnly(projection);
4407
4425
  const base = args;
4408
4426
  // One statement when the rows fit the engine's per-statement limits,
4409
4427
  // else chunks that do, all in one transaction so the rows land together
4410
4428
  // or not at all. (`buildCreateMany`, one statement for a pipeline,
4411
4429
  // refuses instead.)
4412
- const { chunks, rowsPerChunk } = writesMod.planCreateMany(this.ctx, base, projection);
4430
+ // Splitting a call that fits one statement needs a transaction, which a
4431
+ // pool without connect() cannot open outside one.
4432
+ const canSplit = this.txScoped || typeof this.pool.connect === 'function';
4433
+ const { chunks, rowsPerChunk } = writesMod.planCreateMany(this.ctx, base, projection, canSplit);
4413
4434
  const run = async () => {
4414
4435
  // An engine that applies two rows whose onConflict keys are equal only
4415
4436
  // under the column's collation (SQLite's NOCASE) as an insert and an
@@ -4425,13 +4446,18 @@ export class QueryInterface {
4425
4446
  const reselects = this.dialect.resultStrategy === 'reselect';
4426
4447
  return this.inOneTransaction(async (exec) => {
4427
4448
  const rows = [];
4449
+ let count = 0;
4428
4450
  for (const chunk of chunks) {
4429
4451
  const result = reselects && chunk.reselect
4430
4452
  ? await chunk.reselect(exec)
4431
4453
  : await exec(chunk.sql, chunk.params, chunk.preparedName);
4432
- rows.push(...chunk.transform(result));
4454
+ const out = chunk.transform(result);
4455
+ if (countOnly)
4456
+ count += out.count;
4457
+ else
4458
+ rows.push(...out);
4433
4459
  }
4434
- return rows;
4460
+ return (countOnly ? { count } : rows);
4435
4461
  }, args.timeout, `createMany of ${base.data.length} rows`, `Insert at most ${rowsPerChunk} rows per call, which fits one statement, or pass a pool that can check ` +
4436
4462
  'out a connection.');
4437
4463
  };
@@ -4721,17 +4747,6 @@ export class QueryInterface {
4721
4747
  preparedName: entry.name,
4722
4748
  };
4723
4749
  }
4724
- // -------------------------------------------------------------------------
4725
- // groupBy (with aggregate functions)
4726
- // -------------------------------------------------------------------------
4727
- /**
4728
- * Group rows and compute per-group aggregates (Prisma-style). The result row
4729
- * type is INFERRED from the args: each `by` field carries its entity field
4730
- * type, `_count` is always a number, and each requested `_sum` / `_avg` /
4731
- * `_min` / `_max` block maps its fields to properly typed values (see
4732
- * {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
4733
- * that cannot be typed, so those columns are not projected onto the row type.
4734
- */
4735
4750
  async groupBy(args) {
4736
4751
  args = this.normalizeArgs(args);
4737
4752
  return this.executeWithMiddleware('groupBy', args, async () => {
@@ -4740,9 +4755,6 @@ export class QueryInterface {
4740
4755
  return deferred.transform(result);
4741
4756
  });
4742
4757
  }
4743
- // ---------------------------------------------------------------------------
4744
- // Aggregate / groupBy compilation (extracted to aggregates.ts).
4745
- // ---------------------------------------------------------------------------
4746
4758
  buildGroupBy(args) {
4747
4759
  const normalized = this.normalizeArgs(args);
4748
4760
  return this.withPreparedName(() => aggMod.buildGroupBy(this.ctx, normalized));
@@ -4751,9 +4763,6 @@ export class QueryInterface {
4751
4763
  const normalized = this.normalizeArgs(args);
4752
4764
  return this.withPreparedName(() => aggMod.buildAggregate(this.ctx, normalized));
4753
4765
  }
4754
- // -------------------------------------------------------------------------
4755
- // aggregate, standalone aggregation without groupBy
4756
- // -------------------------------------------------------------------------
4757
4766
  async aggregate(args) {
4758
4767
  args = this.normalizeArgs(args);
4759
4768
  return this.executeWithMiddleware('aggregate', args, async () => {
@@ -124,6 +124,19 @@ export declare const expr: Readonly<{
124
124
  * value). Refused anywhere else.
125
125
  */
126
126
  excluded<F extends string>(field: F): ExcludedRef<F>;
127
+ /**
128
+ * The seconds from `from` to `to`, two timestamp columns of the row:
129
+ * `to - from`, fractional, negative when `to` is earlier, NULL when either is.
130
+ * Both columns must be timestamptz, or both zone-less timestamp. Accepted in
131
+ * `_percentile`'s `of` (PostgreSQL); refused in update data in this release.
132
+ *
133
+ * ```ts
134
+ * await db.jobs.aggregate({
135
+ * _percentile: { wait: { of: expr.secondsBetween(col('requestedAt'), col('readyAt')), p: [0.5, 0.95] } },
136
+ * });
137
+ * ```
138
+ */
139
+ secondsBetween(from: ColumnRef, to: ColumnRef): SqlExpr<number>;
127
140
  }>;
128
141
  /** True only for a node built by {@link expr} (any loaded copy of the package). */
129
142
  export declare function isSqlExpr(value: unknown): value is SqlExpr;
@@ -167,6 +180,12 @@ export interface ExprCompileEnv {
167
180
  readonly insertColumns?: ReadonlySet<string> | undefined;
168
181
  /** Compile a `case` arm's predicate (already pruned and validated) against the target row. */
169
182
  predicate(when: Record<string, unknown>): string;
183
+ /**
184
+ * The expression is READ by a query (`_percentile`'s `of`), not assigned:
185
+ * there is no target column, `targetKey` is the result's name, and
186
+ * `expr.secondsBetween` is accepted while `expr.now` is not.
187
+ */
188
+ readonly read?: true | undefined;
170
189
  }
171
190
  /** One compiled expression. */
172
191
  export interface CompiledSetExpr {
@@ -197,6 +216,34 @@ export interface CompiledSetExpr {
197
216
  * cache key does not capture.
198
217
  */
199
218
  export declare function compileSetExpr(env: ExprCompileEnv, value: SqlExpr | ColumnRef): CompiledSetExpr;
219
+ /** What {@link compileReadExpr} needs from its caller (the aggregate module). */
220
+ export interface ReadExprEnv {
221
+ readonly qi: BuilderCtx;
222
+ /** The result's name, for messages. */
223
+ readonly name: string;
224
+ /** For messages: `aggregate _percentile`, `groupBy _percentile`. */
225
+ readonly operation: string;
226
+ /** Bind one number, bigint or null and return its placeholder, cast to its class's type. */
227
+ bind(value: number | bigint | null, cls: 'int' | 'dec'): string;
228
+ /** Compile a `case` arm's predicate (already pruned and validated) against the row. */
229
+ predicate(when: Record<string, unknown>): string;
230
+ }
231
+ /** One compiled read expression. */
232
+ export interface CompiledReadExpr {
233
+ readonly sql: string;
234
+ /** Value-free shape, as {@link CompiledSetExpr.fingerprint}. */
235
+ readonly fingerprint: string;
236
+ /** Columns the expression reads as a value (columns only a case arm's `when` reads are not). */
237
+ readonly values: ReadonlySet<string>;
238
+ }
239
+ /**
240
+ * Compile a numeric expression a query READS (`_percentile`'s `of`): the walk
241
+ * {@link compileSetExpr} runs, with no target column. Bound values must be
242
+ * numbers or bigints and are cast by their own class, the result must be
243
+ * numeric, and `col()` alone is refused because a field key already says it.
244
+ * Not gated on `updateExpressions`: the caller's own engine check applies.
245
+ */
246
+ export declare function compileReadExpr(read: ReadExprEnv, value: unknown): CompiledReadExpr;
200
247
  /**
201
248
  * Rewrite every `expr.excluded(f)` in a single-row upsert's `update` to the
202
249
  * value `create` supplies for `f`. The incoming row of a one-row upsert IS
@@ -44,6 +44,7 @@ const EXPR_KINDS = new Set([
44
44
  'case',
45
45
  'now',
46
46
  'excluded',
47
+ 'secondsBetween',
47
48
  ]);
48
49
  /** Operand count bounds for coalesce / greatest / least. */
49
50
  const MIN_VARIADIC = 2;
@@ -161,6 +162,20 @@ function caseNode(arms, otherwise) {
161
162
  assertOperand('case', arms.length, otherwise);
162
163
  return makeNode('case', frozen, otherwise === undefined ? undefined : { else: otherwise });
163
164
  }
165
+ function secondsBetweenNode(from, to) {
166
+ [from, to].forEach((operand, i) => {
167
+ if (isColumnRef(operand))
168
+ return;
169
+ const at = `expr.secondsBetween(): operand ${i + 1}`;
170
+ if (isPlainColumnRefShape(operand)) {
171
+ const name = JSON.stringify(operand.col);
172
+ throw new ValidationError(`${at} is a plain { col: ${name} } object, not a column reference. Build one with col(${name}).`);
173
+ }
174
+ throw new ValidationError(`${at} is ${operand === undefined ? 'undefined' : describeOperand(operand)}. Both operands are col() of a ` +
175
+ 'timestamp column of the row.');
176
+ });
177
+ return makeNode('secondsBetween', [from, to]);
178
+ }
164
179
  /**
165
180
  * Builders for SQL expressions in update data. Each returns a frozen, branded
166
181
  * node; the SQL is generated per engine when the update compiles.
@@ -246,6 +261,21 @@ export const expr = Object.freeze({
246
261
  Object.defineProperty(node, EXCLUDED_REF, { value: true, enumerable: false });
247
262
  return Object.freeze(node);
248
263
  },
264
+ /**
265
+ * The seconds from `from` to `to`, two timestamp columns of the row:
266
+ * `to - from`, fractional, negative when `to` is earlier, NULL when either is.
267
+ * Both columns must be timestamptz, or both zone-less timestamp. Accepted in
268
+ * `_percentile`'s `of` (PostgreSQL); refused in update data in this release.
269
+ *
270
+ * ```ts
271
+ * await db.jobs.aggregate({
272
+ * _percentile: { wait: { of: expr.secondsBetween(col('requestedAt'), col('readyAt')), p: [0.5, 0.95] } },
273
+ * });
274
+ * ```
275
+ */
276
+ secondsBetween(from, to) {
277
+ return secondsBetweenNode(from, to);
278
+ },
249
279
  });
250
280
  /** True only for a node built by {@link expr} (any loaded copy of the package). */
251
281
  export function isSqlExpr(value) {
@@ -437,6 +467,12 @@ function isBareStructuredCopy(qi, column, value) {
437
467
  const type = columnType(qi, column);
438
468
  return source !== undefined && type !== undefined && columnType(qi, source) === type;
439
469
  }
470
+ /** Where an expression sits, for messages. */
471
+ function site(env) {
472
+ return env.read
473
+ ? `${env.operation} "${env.targetKey}" on "${env.qi.table}"`
474
+ : `${env.operation} data on "${env.qi.table}.${env.targetKey}"`;
475
+ }
440
476
  /**
441
477
  * Compile one expression (or `col()` reference) assigned to `env.targetKey`.
442
478
  * The single traversal: SQL, params (through `env.bind`, in emission order)
@@ -457,7 +493,7 @@ export function compileSetExpr(env, value) {
457
493
  const { qi } = env;
458
494
  assertEngine(qi);
459
495
  const column = assertExprTarget(qi, env.targetKey, value, env.operation);
460
- const state = { env, targetType: columnType(qi, column), reads: new Set() };
496
+ const state = { env, targetType: columnType(qi, column), reads: new Set(), values: new Set() };
461
497
  const part = walkOperand(state, value, false, 0);
462
498
  if (part.cls === 'dec' && numClassOfType(state.targetType) === 'int') {
463
499
  const what = isColumnRef(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
@@ -469,6 +505,48 @@ export function compileSetExpr(env, value) {
469
505
  }
470
506
  return { sql: part.sql, fingerprint: part.fp, reads: state.reads };
471
507
  }
508
+ /**
509
+ * Compile a numeric expression a query READS (`_percentile`'s `of`): the walk
510
+ * {@link compileSetExpr} runs, with no target column. Bound values must be
511
+ * numbers or bigints and are cast by their own class, the result must be
512
+ * numeric, and `col()` alone is refused because a field key already says it.
513
+ * Not gated on `updateExpressions`: the caller's own engine check applies.
514
+ */
515
+ export function compileReadExpr(read, value) {
516
+ const { qi, name, operation } = read;
517
+ const at = `${operation} "${name}" on "${qi.table}"`;
518
+ if (isColumnRef(value)) {
519
+ throw new ValidationError(`${at}: \`of\` is col(${JSON.stringify(value.col)}) alone. Use the field as the key instead: ` +
520
+ `{ ${JSON.stringify(value.col)}: { p } }.`);
521
+ }
522
+ if (!isSqlExpr(value)) {
523
+ throw new ValidationError(`${at}: \`of\` is ${value === undefined ? 'undefined' : describeOperand(value)}. Build it with expr ` +
524
+ "(import { col, expr } from 'turbine-orm'); a plain object is never an expression.");
525
+ }
526
+ const env = {
527
+ qi,
528
+ targetKey: name,
529
+ operation,
530
+ read: true,
531
+ bind: (leaf, arithmetic) => {
532
+ if (leaf !== null && typeof leaf !== 'number' && typeof leaf !== 'bigint') {
533
+ throw new ValidationError(`${at}: a bound value is ${leaf instanceof Date ? 'a Date' : leaf instanceof Uint8Array ? 'a Uint8Array' : `a ${typeof leaf}`}. ` +
534
+ 'An expression a query reads binds numbers and bigints only.');
535
+ }
536
+ const integer = (arithmetic ?? numClassOfValue(leaf)) !== 'dec' && (typeof leaf !== 'number' || Number.isSafeInteger(leaf));
537
+ return read.bind(leaf, integer ? 'int' : 'dec');
538
+ },
539
+ column: (column) => qi.q(column),
540
+ predicate: (when) => read.predicate(when),
541
+ };
542
+ const state = { env, targetType: undefined, reads: new Set(), values: new Set() };
543
+ const part = walkOperand(state, value, false, 0);
544
+ if (part.cls === 'other' || part.cls === 'null') {
545
+ throw new ValidationError(`${at}: \`of\` is ${part.cls === 'null' ? 'NULL for every row' : 'not numeric (it reads a non-numeric column)'}. ` +
546
+ 'A percentile orders numbers: numeric columns, bound numbers, and expr.secondsBetween() of two timestamps.');
547
+ }
548
+ return { sql: part.sql, fingerprint: part.fp, values: state.values };
549
+ }
472
550
  function assertEngine(qi) {
473
551
  if (qi.dialect.capabilities.updateExpressions)
474
552
  return;
@@ -481,6 +559,7 @@ function walkOperand(state, value, inArithmetic, depth) {
481
559
  if (isColumnRef(value)) {
482
560
  const column = resolveOperandColumn(state, value.col, 'col', inArithmetic);
483
561
  state.reads.add(column);
562
+ state.values.add(column);
484
563
  return { sql: state.env.column(column), fp: `c:${column}`, cls: numClassOfType(columnType(state.env.qi, column)) };
485
564
  }
486
565
  if (isSqlExpr(value))
@@ -492,7 +571,7 @@ function walkOperand(state, value, inArithmetic, depth) {
492
571
  // greatest, least or a case arm (`expr.add(expr.coalesce('x', 1), 2)`).
493
572
  if (inArithmetic &&
494
573
  (typeof value === 'string' || typeof value === 'boolean' || value instanceof Date || value instanceof Uint8Array)) {
495
- throw new ValidationError(`An arithmetic operand in ${state.env.operation} data on "${state.env.qi.table}.${state.env.targetKey}" is a ` +
574
+ throw new ValidationError(`An arithmetic operand in ${site(state.env)} is a ` +
496
575
  `${value instanceof Date ? 'Date' : value instanceof Uint8Array ? 'Uint8Array' : typeof value}. ` +
497
576
  'Arithmetic takes numbers, bigints, col() of a numeric column, or numeric expressions.');
498
577
  }
@@ -509,7 +588,7 @@ function resolveOperandColumn(state, field, how, inArithmetic) {
509
588
  const call = how === 'col' ? `col(${JSON.stringify(field)})` : `expr.excluded(${JSON.stringify(field)})`;
510
589
  const column = resolveColumnName(qi.tableMeta, field);
511
590
  if (column === undefined) {
512
- throw new ValidationError(`${call} in ${state.env.operation} data on "${qi.table}" names no column. ` +
591
+ throw new ValidationError(`${call} in ${site(state.env)} names no column. ` +
513
592
  `Known fields: ${Object.keys(qi.tableMeta.columnMap).join(', ') || '(none)'}.`);
514
593
  }
515
594
  const cls = columnClass(columnType(qi, column));
@@ -569,6 +648,9 @@ function walkNode(state, node, inArithmetic, depth) {
569
648
  case 'case':
570
649
  return walkCase(state, node, inArithmetic, depth);
571
650
  case 'now': {
651
+ if (env.read) {
652
+ throw new ValidationError(`expr.now() in ${site(env)}: an expression a query reads takes the row's columns and bound numbers only.`);
653
+ }
572
654
  if (inArithmetic) {
573
655
  throw new ValidationError('expr.now() cannot be an arithmetic operand: date arithmetic is not supported.');
574
656
  }
@@ -592,6 +674,8 @@ function walkNode(state, node, inArithmetic, depth) {
592
674
  }
593
675
  return { sql: env.excluded(column), fp: `x:${column}`, cls: numClassOfType(columnType(qi, column)) };
594
676
  }
677
+ case 'secondsBetween':
678
+ return walkSecondsBetween(state, node);
595
679
  default:
596
680
  throw new ValidationError(`Unknown expression kind "${String(node.kind)}".`);
597
681
  }
@@ -623,6 +707,47 @@ function walkCase(state, node, inArithmetic, depth) {
623
707
  cls: combineClasses(classes),
624
708
  };
625
709
  }
710
+ /**
711
+ * `expr.secondsBetween(from, to)`: `EXTRACT(EPOCH FROM (to - from))`, the
712
+ * PostgreSQL spelling. It is read only by `_percentile`, which only an engine
713
+ * with `supportsPercentileAggregates` runs, and that SQL (`percentile_cont ...
714
+ * WITHIN GROUP`) is PostgreSQL's too. Both columns must have one temporal
715
+ * kind: subtracting a timestamptz from a zone-less timestamp converts through
716
+ * the session time zone.
717
+ */
718
+ function walkSecondsBetween(state, node) {
719
+ const { env } = state;
720
+ const { qi } = env;
721
+ if (!env.read) {
722
+ throw new ValidationError(`expr.secondsBetween() in ${site(env)}: it is accepted in _percentile's \`of\` in this release, not in ` +
723
+ 'update data. Compute the value in JavaScript and write it as a plain value.');
724
+ }
725
+ const kinds = [];
726
+ const columns = node.args.map((arg) => {
727
+ const field = arg.col;
728
+ const column = resolveOperandColumn(state, field, 'col', false);
729
+ const type = columnType(qi, column);
730
+ const kind = temporalKind(type);
731
+ if (kind !== 'tz' && kind !== 'timestamp') {
732
+ throw new ValidationError(`expr.secondsBetween() in ${site(env)}: col(${JSON.stringify(field)}) is ${type === undefined ? 'a column of unknown type' : `a ${type} column`}. ` +
733
+ 'Both operands must be timestamp (or both timestamptz) columns.');
734
+ }
735
+ kinds.push(kind);
736
+ state.reads.add(column);
737
+ state.values.add(column);
738
+ return column;
739
+ });
740
+ if (kinds[0] !== kinds[1]) {
741
+ throw new ValidationError(`expr.secondsBetween() in ${site(env)} mixes a timestamptz column with a zone-less timestamp column, ` +
742
+ 'whose difference depends on the session time zone. Use two columns of one kind.');
743
+ }
744
+ const [from, to] = columns;
745
+ return {
746
+ sql: `EXTRACT(EPOCH FROM (${env.column(to)} - ${env.column(from)}))`,
747
+ fp: `secs(${from},${to})`,
748
+ cls: 'dec',
749
+ };
750
+ }
626
751
  /**
627
752
  * Prune and validate one arm's `when`: `undefined` keys drop out (the C1 rule
628
753
  * for every `where` position), an arm left with no predicate is refused, and
@@ -5,7 +5,7 @@
5
5
  * `import { … } from './query/index.js'` is a drop-in replacement for the
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
- export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonEncoding, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderBySpec, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationOrderBy, RelationOrderByChain, RelationPickBy, RelationPickOrderBy, ResolvedSkipGlobalFilters, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, CreateManyCount, CreateManyResult, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonEncoding, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderBySpec, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationOrderBy, RelationOrderByChain, RelationPickBy, RelationPickOrderBy, ResolvedSkipGlobalFilters, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
11
  export { col } from './column-ref.js';
@@ -136,6 +136,9 @@ export const CREATE_MANY_OPTIONS = {
136
136
  onConflict: 'internal',
137
137
  select: 'internal',
138
138
  omit: 'internal',
139
+ // Compat's createMany already resolves to Prisma's { count } and sets this
140
+ // itself; a caller-supplied value would change the shape it relies on.
141
+ returning: 'internal',
139
142
  timeout: 'native',
140
143
  skipGlobalFilters: 'native',
141
144
  };