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
package/README.md CHANGED
@@ -21,39 +21,38 @@ Six reasons, each with the mechanism that makes it true:
21
21
  1. **One dependency.** `dependencies` is `{ "pg": "^8.15.0" }`. No engine binary, no WASM compiler, no adapter packages in lockstep. The optional engines (SQLite, MySQL, SQL Server, PowDB, all tiered Experimental in [STABILITY.md](STABILITY.md)) are peer dependencies or Node builtins you install only if you use them.
22
22
  2. **Written from scratch.** Turbine is not a layer over Knex or a query-builder library. Query compilation is plain string building with an FNV-1a shape fingerprint into a bounded LRU of SQL templates, so there is no plan cache to size and no compiler running on your event loop.
23
23
  3. **Nested relations in one statement.** A `with` clause compiles to correlated `json_agg` subqueries, so users with posts with comments is one round trip, typed end to end: `users[0].posts[0].comments[0].author.name` autocompletes with no annotation.
24
- 4. **Close to raw SQL.** In the last published run, Turbine's overhead over a hand-written `pg` control was 1.08x by geometric mean. The table is below; the losses are stated with the wins.
24
+ 4. **Faster than Drizzle and Prisma.** In the 0.91.0 run Drizzle took 1.4x as long as Turbine and Prisma 2.0x, by geometric mean, on a dedicated benchmark machine. The table is below; the losses are stated with the wins.
25
25
  5. **Agents get a skill and typed tools, not a SQL prompt.** `npx turbine skill` installs a query-writing skill whose every claim is executed against a live database before release; `npx turbine mcp` exposes eleven read-only MCP tools, including a relation graph and a join-path finder that returns the `with` clause to write. Every tool runs inside `BEGIN READ ONLY`, and PII-tagged columns are redacted before rows reach a model.
26
26
  6. **The dangerous operations ask first.** Destructive migration statements refuse to run without typed consent. `update`/`delete` with an empty `where` throws. Columns tagged `pii: true` are excluded from the emitted SQL's projections. `turbine doctor` reports missing FK indexes offline, no account, no telemetry.
27
27
 
28
28
  ## Benchmarks
29
29
 
30
- Measured **2026-08-15 against turbine-orm 0.71.0**, versus **Prisma 7.9.1** (`@prisma/adapter-pg`, `relationJoins` on) and **Drizzle 0.45.2** (relational queries), on local **PostgreSQL 17.9** over a Unix socket, with a hand-written `pg` control arm. Node v24.18.0, Apple Silicon (M5 Max). Same schema, same data (1K users, 10K posts, 50K comments), same pool config. Each figure is the median over 200 rounds with arm order rotated per round, taken as the median of three full runs.
30
+ Measured **2026-10-15 against turbine-orm 0.91.0**, versus **Drizzle 0.45.3** (relational queries) and **Prisma 7.10.0** (`@prisma/adapter-pg`, `relationJoins` on), with a hand-written `pg` control arm. Run by the repository's [`benchmarks.yml`](.github/workflows/benchmarks.yml) workflow on a dedicated machine: AMD Ryzen 9 7950X host, 4-vCPU 16 GB Firecracker VM, client and PostgreSQL 17 (Docker) on the same VM over a Unix socket. PostgreSQL 17.11, Node v24.18.0, `pg` 8.23.1 under every arm. Same schema, same data (1K users, 10K posts, 50K comments), same pool config. Each figure is the median over 200 rounds with arm order rotated per round, taken as the median of 3 full runs.
31
31
 
32
- | Scenario | Turbine 0.71 | Prisma 7.9 | Drizzle 0.45 | raw pg |
32
+ | Scenario | Turbine 0.91.0 | Drizzle 0.45.3 | Prisma 7.10.0 | raw pg |
33
33
  |---|---|---|---|---|
34
- | findMany, 100 users (flat) | **0.181 ms** | 0.241 ms | 0.242 ms | 0.164 ms |
35
- | findMany, 50 users + posts (L2) *(contested)* | **1.670 ms** | 4.302 ms | 1.711 ms | 1.847 ms |
36
- | findMany, 10 users → posts → comments (L3) *(contested)* | **1.082 ms** | 4.204 ms | 1.166 ms | n/a |
37
- | findUnique, single user by PK | **0.035 ms** | 0.078 ms | 0.080 ms | 0.036 ms |
38
- | findUnique, user + posts + comments (L3) | **0.146 ms** | 0.374 ms | 0.285 ms | n/a |
39
- | count, all users | **0.038 ms** | 0.070 ms | 0.053 ms | 0.041 ms |
40
- | stream, iterate 50K rows (batch 1000) | 50.39 ms | 54.81 ms | **39.49 ms** | 42.33 ms |
41
- | atomic increment, `view_count + 1` *(contested)* | **0.067 ms** | 0.106 ms | 0.073 ms | 0.055 ms |
42
- | pipeline, 5-query batch | **0.165 ms** | 0.338 ms | 0.341 ms | 0.177 ms |
43
- | hot findUnique, 500x same shape | **0.026 ms** | 0.059 ms | 0.069 ms | 0.030 ms |
34
+ | findMany, 100 users | **0.204 ms** | 0.307 ms | 0.349 ms | 0.198 ms |
35
+ | findMany, 50 users + posts *(tie)* | 1.470 ms | 1.488 ms | 2.930 ms | 0.791 ms |
36
+ | findMany, 10 users + posts + comments | **0.795 ms** | 0.899 ms | 2.303 ms | n/a |
37
+ | findUnique by primary key | **0.090 ms** | 0.151 ms | 0.179 ms | 0.074 ms |
38
+ | findUnique, user + posts + comments | **0.278 ms** | 0.467 ms | 0.599 ms | n/a |
39
+ | count, all users | **0.080 ms** | 0.115 ms | 0.170 ms | 0.077 ms |
40
+ | stream 50,000 comments, per row *(tie)* | 55.27 ms | 57.33 ms | 85.99 ms | 56.38 ms |
41
+ | atomic increment | **0.175 ms** | 0.185 ms | 0.274 ms | 0.143 ms |
42
+ | pipeline, 5-query batch | **0.333 ms** | 0.707 ms | 0.782 ms | 0.365 ms |
43
+ | hot findUnique, rotating ids | **0.078 ms** | 0.143 ms | 0.159 ms | 0.072 ms |
44
44
 
45
45
  What that run says:
46
46
 
47
- - **Turbine ran at 1.08x hand-written `pg`**, where Drizzle ran at 1.47x and Prisma at 1.81x (geometric mean over the eight scenarios with a raw control). That 1.08x is the conservative reading: the raw L2 control statement still uses the `json_build_object` encoding Turbine has moved off, which is worth 1.83x on its own, so the figure the harness records unadjusted is 1.00x. The adjusted one is published instead.
48
- - Across all ten scenarios Turbine was **2.02x faster than Prisma 7.9** and **1.46x faster than Drizzle 0.45** by geometric mean. Turbine took nine scenarios, Drizzle one, Prisma none. The one Drizzle took was streaming, and 0.72.0 takes it back (below).
49
- - **Streaming was Drizzle's one win, by 22%** (39.49 ms vs 50.39 ms) on 0.71.0. **0.72.0 closes it**: the remaining gap was temporal decoding rather than the cursor, and a direct scan for the canonical ISO wire shape (declining anything else back to `pg`'s own parser) takes `findManyStream` from 52.44 ms to **37.06 ms** against Drizzle's 42.09 ms re-measured in the same rotation, and `findManyStreamBatches` to **31.65 ms**. Scope, because it matters: that is a targeted re-measurement of one scenario on a paired one-process harness, not a re-run of the ten above, so the table's stream row still shows 0.71.0 and the absolute values are not comparable. Method and controls in [`benchmarks/RESULTS-0.72.0.md`](benchmarks/RESULTS-0.72.0.md).
50
- - **Pipelining is Turbine's clearest win**: one TCP flush for 5 queries runs 2.04x faster than Prisma and 2.07x faster than Drizzle, level with raw `pg`.
51
- - Three scenarios (L2, L3, atomic increment) are marked *contested*: the contiguous cross-check harness disagrees with itself across runs of the same configuration there, so neither side claims them. Both nested reads were losses in the previous run and the improvement behind them reproduces on both harnesses; whether it is enough to pass Drizzle is what is unsettled.
52
- - The sub-0.15 ms scenarios carry roughly one third uncertainty in their absolute values. Orderings held across runs; margins should not be quoted, and that includes L2's 2.5%.
47
+ - **Across all ten scenarios Drizzle took 1.40x as long as Turbine and Prisma 2.00x**, by geometric mean. Turbine won or tied every one against Drizzle (2 ties) and won all ten against Prisma.
48
+ - **Against hand-written `pg`, Turbine ran at 1.14x, Drizzle at 1.61x and Prisma at 2.17x** (geometric mean over the 8 scenarios with a control). Where a query returns nested rows, the control decodes them into objects with `Date` timestamps as the ORMs do. The 0.71.0 figure of 1.08x was adjusted by hand against a control that did more work than Turbine; this one is not adjusted.
49
+ - **Pipelining is the clearest win over a network**: five dashboard queries 25 ms from the database take 25 ms as one batch against 171 ms as a transaction.
50
+ - **Where Turbine loses:** deep `OFFSET` pagination unless `forceCustomPlan` is set, and per-row streaming against the Drizzle 1.0 release candidate (`findManyStreamBatches` ties it).
51
+ - Figures under 0.15 ms carry roughly one third uncertainty in their absolute values. Orderings held across runs; those margins should not be quoted.
53
52
 
54
- Full method, the drift-floor measurement, and the harness-disagreement table: [`benchmarks/RESULTS-0.71.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.71.0.md).
53
+ Full method, the extra scenarios, streaming, latency and the losses: [`benchmarks/RESULTS-0.91.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.91.0.md), or the charts at [turbineorm.dev/benchmarks](https://turbineorm.dev/benchmarks).
55
54
 
56
- Reproduce it: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench-interleaved.ts`
55
+ Reproduce it on a fresh machine with `gh workflow run benchmarks.yml`, or locally with `benchmarks/run-round.sh` (see [`benchmarks/README.md`](benchmarks/README.md)).
57
56
 
58
57
  ## Quick Start
59
58
 
@@ -272,7 +271,7 @@ Going deep on one database means the parts other ORMs push to raw SQL are typed
272
271
 
273
272
  ## Serverless and edge
274
273
 
275
- The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **146 kB brotli** (edge entry under **114 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
274
+ The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **148 kB brotli** (edge entry under **116 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
276
275
 
277
276
  ```typescript
278
277
  import { Pool } from '@neondatabase/serverless';
@@ -359,11 +358,11 @@ It is also built to be extended rather than wrapped. All SQL generation routes t
359
358
  |---|---|---|---|---|---|
360
359
  | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine | Kysely as query runner; SQL built by MikroORM (v7) |
361
360
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 | `@mikro-orm/core` (0 deps) + a driver package |
362
- | **Main bundle (brotli)** | under 146 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
361
+ | **Main bundle (brotli)** | under 148 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
363
362
  | **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None | AdminJS integration guide (CRUD panel) |
364
363
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors | |
365
364
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None | Generated by diffing entities against a snapshot or the database; TS/JS classes |
366
- | **Edge runtime** | One import swap, under 114 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
365
+ | **Edge runtime** | One import swap, under 116 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
367
366
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual | `flush()` runs queued changes in one transaction |
368
367
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None | `DriverException` subclasses (`UniqueConstraintViolationException`, ...) |
369
368
  | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) | `populate`; default `balanced`: joins to-one, one query per to-many relation |
@@ -501,6 +501,17 @@ export interface EngineCapabilities {
501
501
  * not bound by it.
502
502
  */
503
503
  readonly maxInsertRows: number;
504
+ /**
505
+ * The most bind parameters each statement of a `createMany` that runs as
506
+ * several statements carries: at most `maxBindParameters`, and lower where
507
+ * an engine slows faster than linearly with a statement's size. SQLite
508
+ * finds each named parameter by a linear search, at prepare and at bind, so
509
+ * 40,000 six-column rows took 141 s in statements of 32,766 parameters and
510
+ * 1 s in statements of 250. A call whose rows cannot be told apart
511
+ * across statements, or on a pool with no `connect()`, is not split below
512
+ * `maxBindParameters` (see `planCreateMany`).
513
+ */
514
+ readonly chunkBindParameters: number;
504
515
  /**
505
516
  * What a single-statement upsert conflicts on. `'explicit'`: the conflict
506
517
  * target it names. `'any-unique-key'`: whichever unique key collides first
@@ -531,6 +542,16 @@ export interface EngineCapabilities {
531
542
  * it afterwards, so the statement runs in a transaction it can roll back.
532
543
  */
533
544
  readonly bulkUpsertAppliesRepeatedKeys: boolean;
545
+ /**
546
+ * A `skipDuplicates` insert reports a skipped row as affected, so its row
547
+ * count cannot be the rows it inserted. MySQL: its skip is a no-op
548
+ * `ON DUPLICATE KEY UPDATE`, which under `CLIENT_FOUND_ROWS` (mysql2's
549
+ * default) counts a matched row, and a one-row statement's result carries no
550
+ * `info` to subtract the duplicates from. `createMany` refuses
551
+ * `skipDuplicates` with `returning: false` here (E017). False on an engine
552
+ * that never runs a skip.
553
+ */
554
+ readonly skipDuplicatesCountsSkipped: boolean;
534
555
  /** `$transaction({ readOnly: true })`: `READ ONLY` after the dialect's BEGIN. Else E017. */
535
556
  readonly readOnlyTransactions: boolean;
536
557
  /**
@@ -817,7 +838,11 @@ export interface Dialect {
817
838
  readonly explainQuery?: {
818
839
  prefix: string;
819
840
  };
820
- /** Build a dialect-specific RETURNING clause. Return an empty string when unsupported. */
841
+ /**
842
+ * Build a dialect-specific RETURNING clause. Return an empty string when
843
+ * unsupported, and for an empty column list (a write that returns no column,
844
+ * `createMany({ returning: false })`).
845
+ */
821
846
  buildReturningClause(selection?: ReturningSelection): string;
822
847
  /** Build a single-row INSERT statement. Inputs are SQL-ready quoted fragments. */
823
848
  buildInsertStatement(input: InsertStatementInput): string;
@@ -108,12 +108,14 @@ exports.postgresDialect = {
108
108
  // Counted in a 16-bit field.
109
109
  maxBindParameters: 65535,
110
110
  maxInsertRows: Number.POSITIVE_INFINITY,
111
+ chunkBindParameters: 65535,
111
112
  upsertConflictTarget: 'explicit',
112
113
  setClauseOrder: 'snapshot',
113
114
  reselectLocksRows: false,
114
115
  integerDivision: '/',
115
116
  updateExpressions: true,
116
117
  bulkUpsertAppliesRepeatedKeys: false,
118
+ skipDuplicatesCountsSkipped: false,
117
119
  readOnlyTransactions: true,
118
120
  connectionParameters: true,
119
121
  backendCancel: true,
@@ -191,6 +193,8 @@ exports.postgresDialect = {
191
193
  return `COALESCE((${subquery}), ${fallback})`;
192
194
  },
193
195
  buildReturningClause(selection = '*') {
196
+ if (selection !== '*' && selection.length === 0)
197
+ return '';
194
198
  return ` RETURNING ${selection === '*' ? '*' : selection.join(', ')}`;
195
199
  },
196
200
  buildInsertStatement(input) {
@@ -47,9 +47,9 @@ export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type Me
47
47
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
48
48
  export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
49
49
  export { type CaseArm, type ExprOperand, expr, type NumericOperand } from './query/expr.js';
50
- export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, col, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type JsonValue, jsonValue, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderBySpec, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationOrderBy, type RelationOrderByChain, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
50
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type CreateManyCount, type CreateManyResult, col, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type JsonValue, jsonValue, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderBySpec, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationOrderBy, type RelationOrderByChain, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
51
51
  export type { SqlCommentOptions } from './query/sql-comment.js';
52
- export type { AggregateResultOf, ConflictTarget, ConflictUpdateData, CreateManyOnConflict, DefaultInputTypes, ExcludedRef, GlobalFilter, NestedToOneCreateOp, NestedToOneUpdateOp, OptionalInput, PercentileMethod, PercentileSpec, RelationKind, RowLock, RowLockMode, SqlExpr, SqlExprKind, TableInputTypes, } from './query/types.js';
52
+ export type { AggregateResultOf, ConflictTarget, ConflictUpdateData, CreateManyOnConflict, DefaultInputTypes, ExcludedRef, FieldPercentiles, GlobalFilter, NestedToOneCreateOp, NestedToOneUpdateOp, OptionalInput, PercentileArgs, PercentileExprSpec, PercentileKeysChecked, PercentileMethod, PercentileSpec, RelationKind, RowLock, RowLockMode, SqlExpr, SqlExprKind, TableInputTypes, } from './query/types.js';
53
53
  export { MAX_QUERY_TAG_LENGTH, RAW_QUERY_MODEL } from './query-events.js';
54
54
  export { type ActiveSubscription, type ListenOptions, type ListenReconnectOptions, type NotificationHandler, type Subscription, validateChannel, } from './realtime.js';
55
55
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
package/dist/cjs/mssql.js CHANGED
@@ -644,6 +644,7 @@ exports.mssqlDialect = {
644
644
  // The limit the driver can actually send (see MSSQL_MAX_PARAMS), not 2100.
645
645
  maxBindParameters: MSSQL_MAX_PARAMS,
646
646
  maxInsertRows: MSSQL_MAX_INSERT_ROWS,
647
+ chunkBindParameters: MSSQL_MAX_PARAMS,
647
648
  upsertConflictTarget: 'explicit',
648
649
  setClauseOrder: 'snapshot',
649
650
  reselectLocksRows: false,
@@ -651,6 +652,7 @@ exports.mssqlDialect = {
651
652
  updateExpressions: true,
652
653
  // MERGE refuses a target row matched twice (error 8672).
653
654
  bulkUpsertAppliesRepeatedKeys: false,
655
+ skipDuplicatesCountsSkipped: false,
654
656
  readOnlyTransactions: false,
655
657
  connectionParameters: false,
656
658
  backendCancel: false,
package/dist/cjs/mysql.js CHANGED
@@ -709,6 +709,7 @@ exports.mysqlDialect = {
709
709
  // Counted in a 16-bit field.
710
710
  maxBindParameters: 65535,
711
711
  maxInsertRows: Number.POSITIVE_INFINITY,
712
+ chunkBindParameters: 65535,
712
713
  upsertConflictTarget: 'any-unique-key',
713
714
  setClauseOrder: 'left-to-right',
714
715
  reselectLocksRows: true,
@@ -716,6 +717,8 @@ exports.mysqlDialect = {
716
717
  updateExpressions: true,
717
718
  // Caught by the onConflict re-read, which runs in the reselect transaction.
718
719
  bulkUpsertAppliesRepeatedKeys: true,
720
+ // A skipped row is a no-op ON DUPLICATE KEY UPDATE, counted under FOUND_ROWS.
721
+ skipDuplicatesCountsSkipped: true,
719
722
  readOnlyTransactions: true,
720
723
  connectionParameters: false,
721
724
  backendCancel: false,
package/dist/cjs/powdb.js CHANGED
@@ -147,12 +147,14 @@ exports.powdbDialect = {
147
147
  structuredValueColumns: 'scalars',
148
148
  maxBindParameters: Number.POSITIVE_INFINITY,
149
149
  maxInsertRows: Number.POSITIVE_INFINITY,
150
+ chunkBindParameters: Number.POSITIVE_INFINITY,
150
151
  upsertConflictTarget: 'explicit',
151
152
  setClauseOrder: 'snapshot',
152
153
  reselectLocksRows: false,
153
154
  integerDivision: '/',
154
155
  updateExpressions: false,
155
156
  bulkUpsertAppliesRepeatedKeys: false,
157
+ skipDuplicatesCountsSkipped: false,
156
158
  readOnlyTransactions: false,
157
159
  connectionParameters: false,
158
160
  backendCancel: false,
@@ -37,7 +37,7 @@
37
37
  import type { PowdbPool } from './powdb.js';
38
38
  import type { MiddlewareFn, QueryInterfaceOptions } from './query/index.js';
39
39
  import type { TableClient } from './query/table-client.js';
40
- import type { AggregateArgs, AggregateResultOf, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindUniqueArgs, GroupByArgs, GroupByResult, OptionalInput, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs } from './query/types.js';
40
+ import type { AggregateArgs, AggregateResultOf, CountArgs, CreateArgs, CreateManyArgs, CreateManyResult, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindUniqueArgs, GroupByArgs, GroupByResult, OptionalInput, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs } from './query/types.js';
41
41
  import { type SchemaMetadata } from './schema.js';
42
42
  /**
43
43
  * Max values one `in` / `notIn` on a PowDB-native `datetime` column may carry.
@@ -1146,7 +1146,10 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
1146
1146
  * byte-identical PowQL.
1147
1147
  */
1148
1148
  private assertUniformCreateManyRows;
1149
- createMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateManyArgs<T, OptionalInput<T>, S, O>): Promise<FieldResult<T, S, O>[]>;
1149
+ createMany<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined, C extends boolean | undefined = undefined>(args: CreateManyArgs<T, OptionalInput<T>, S, O, C>): Promise<CreateManyResult<FieldResult<T, S, O>, C>>;
1150
+ /** `returning: false` validated: a boolean, and not combined with a projection. */
1151
+ private createManyCountOnly;
1152
+ private insertMany;
1150
1153
  update<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, NoRelations, S, O>): Promise<FieldResult<T, S, O>>;
1151
1154
  updateMany(args: UpdateManyArgs<T>): Promise<{
1152
1155
  count: number;
package/dist/cjs/powql.js CHANGED
@@ -3860,35 +3860,56 @@ class PowqlInterface {
3860
3860
  async createMany(args) {
3861
3861
  args = this.normalizeArgs(args);
3862
3862
  return this.withMiddleware('createMany', args, async () => {
3863
- if (args.onConflict !== undefined) {
3864
- throw new errors_js_1.UnsupportedFeatureError('createMany({ onConflict })', 'powdb', "PowQL's upsert statement takes one row. Call upsert() per row inside $transaction.");
3865
- }
3866
- // Validated as every engine validates it; it acts only on onConflict's
3867
- // update, which is refused above.
3868
- (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
3869
- if (args.skipDuplicates) {
3870
- // PowQL's `insert … returning` has no conflict clause, so there is no
3871
- // faithful skip-duplicates form; refuse rather than silently insert.
3872
- throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'powdb', 'PowQL insert has no ON CONFLICT DO NOTHING equivalent, filter duplicates before inserting.');
3873
- }
3874
- // Resolved before the write, so a bad projection writes nothing.
3875
- const plan = this.writeReturnPlan(args);
3876
- const supplied = args.data.map((d) => this.applyPkDefault(d));
3877
- if (!supplied.length)
3878
- return [];
3879
- // Uniformity is judged on what the caller supplied, as the SQL engines
3880
- // judge it; defaults are then filled per row.
3881
- this.assertUniformCreateManyRows(supplied);
3882
- const inputs = supplied.map((d) => this.applyColumnDefaults(d));
3883
- const params = [];
3884
- const tuples = inputs.map((d) => {
3885
- const assigns = this.scalarData(d);
3886
- return `{ ${assigns.map((a) => `${(0, powdb_shared_js_1.quotePowqlIdent)(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`).join(', ')} }`;
3887
- });
3888
- // Multi-row insert with `returning` hands back every inserted row in one round-trip.
3889
- const { rows, native } = await this.exec(`insert ${this.qt} ${tuples.join(', ')} returning`, params, args.timeout, 'createMany');
3890
- return this.shape(rows, native).map((r) => this.shapeWriteRow(r, plan));
3863
+ // `returning: false` is checked as the SQL engines check it. PowQL's
3864
+ // insert still returns the rows, which are counted and dropped.
3865
+ const countOnly = this.createManyCountOnly(args);
3866
+ const rows = await this.insertMany(args);
3867
+ return countOnly ? { count: rows.length } : rows;
3868
+ });
3869
+ }
3870
+ /** `returning: false` validated: a boolean, and not combined with a projection. */
3871
+ createManyCountOnly(args) {
3872
+ const { returning } = args;
3873
+ if (returning === undefined || returning === true)
3874
+ return false;
3875
+ if (returning !== false) {
3876
+ throw new errors_js_1.ValidationError(`createMany on "${this.table}": returning must be true or false, received ${typeof returning}.`);
3877
+ }
3878
+ if (args.select !== undefined || args.omit !== undefined) {
3879
+ throw new errors_js_1.ValidationError(`createMany on "${this.table}": returning: false returns { count } and no rows, so it takes no ` +
3880
+ `\`${args.select !== undefined ? 'select' : 'omit'}\`. Drop one of them.`);
3881
+ }
3882
+ return true;
3883
+ }
3884
+ async insertMany(args) {
3885
+ if (args.onConflict !== undefined) {
3886
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ onConflict })', 'powdb', "PowQL's upsert statement takes one row. Call upsert() per row inside $transaction.");
3887
+ }
3888
+ // Validated as every engine validates it; it acts only on onConflict's
3889
+ // update, which is refused above.
3890
+ (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
3891
+ if (args.skipDuplicates) {
3892
+ // PowQL's `insert … returning` has no conflict clause, so there is no
3893
+ // faithful skip-duplicates form; refuse rather than silently insert.
3894
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'powdb', 'PowQL insert has no ON CONFLICT DO NOTHING equivalent, filter duplicates before inserting.');
3895
+ }
3896
+ // Resolved before the write, so a bad projection writes nothing.
3897
+ const plan = this.writeReturnPlan(args);
3898
+ const supplied = args.data.map((d) => this.applyPkDefault(d));
3899
+ if (!supplied.length)
3900
+ return [];
3901
+ // Uniformity is judged on what the caller supplied, as the SQL engines
3902
+ // judge it; defaults are then filled per row.
3903
+ this.assertUniformCreateManyRows(supplied);
3904
+ const inputs = supplied.map((d) => this.applyColumnDefaults(d));
3905
+ const params = [];
3906
+ const tuples = inputs.map((d) => {
3907
+ const assigns = this.scalarData(d);
3908
+ return `{ ${assigns.map((a) => `${(0, powdb_shared_js_1.quotePowqlIdent)(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`).join(', ')} }`;
3891
3909
  });
3910
+ // Multi-row insert with `returning` hands back every inserted row in one round-trip.
3911
+ const { rows, native } = await this.exec(`insert ${this.qt} ${tuples.join(', ')} returning`, params, args.timeout, 'createMany');
3912
+ return this.shape(rows, native).map((r) => this.shapeWriteRow(r, plan));
3892
3913
  }
3893
3914
  async update(args) {
3894
3915
  args = this.normalizeArgs(args);
@@ -137,7 +137,7 @@ export interface CompatQueryInterface {
137
137
  findFirstOrThrow(args?: Record<string, unknown>): Promise<unknown>;
138
138
  findUniqueOrThrow(args: Record<string, unknown>): Promise<unknown>;
139
139
  create(args: Record<string, unknown>): Promise<unknown>;
140
- createMany(args: Record<string, unknown>): Promise<unknown[]>;
140
+ createMany(args: Record<string, unknown>): Promise<unknown>;
141
141
  update(args: Record<string, unknown>): Promise<unknown>;
142
142
  updateMany(args: Record<string, unknown>): Promise<{
143
143
  count: number;
@@ -156,7 +156,7 @@ export interface CompatQueryInterface {
156
156
  buildFindFirstOrThrow(args?: Record<string, unknown>): DeferredQuery<unknown>;
157
157
  buildFindUniqueOrThrow(args: Record<string, unknown>): DeferredQuery<unknown>;
158
158
  buildCreate(args: Record<string, unknown>): DeferredQuery<unknown>;
159
- buildCreateMany(args: Record<string, unknown>): DeferredQuery<unknown[]>;
159
+ buildCreateMany(args: Record<string, unknown>): DeferredQuery<unknown>;
160
160
  buildUpdate(args: Record<string, unknown>): DeferredQuery<unknown>;
161
161
  buildUpdateMany(args: Record<string, unknown>): DeferredQuery<{
162
162
  count: number;
@@ -1901,12 +1901,12 @@ function createManyRunsOf(t) {
1901
1901
  async function createManyByRun(qi, t, runs) {
1902
1902
  let count = 0;
1903
1903
  for (const run of runs) {
1904
- const args = { data: run };
1904
+ const args = { data: run, returning: false };
1905
1905
  if (t.skipDuplicates)
1906
1906
  args.skipDuplicates = true;
1907
1907
  if (t.timeout !== undefined)
1908
1908
  args.timeout = t.timeout;
1909
- count += (await qi.createMany(args)).length;
1909
+ count += (await qi.createMany(args)).count;
1910
1910
  }
1911
1911
  return { count };
1912
1912
  }
@@ -2002,7 +2002,10 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2002
2002
  `array of them, received ${describeValue(data)}.`);
2003
2003
  }
2004
2004
  const rows = list.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)));
2005
- const t = { data: rows };
2005
+ // Prisma's createMany resolves to { count }, which core counts
2006
+ // without returning the rows (a plain MySQL insert returns none, so
2007
+ // counting rows reported 0 there).
2008
+ const t = { data: rows, returning: false };
2006
2009
  (0, index_js_1.applyNativeOptions)(index_js_1.CREATE_MANY_OPTIONS, args, t);
2007
2010
  if (args.skipDuplicates)
2008
2011
  t.skipDuplicates = true;
@@ -2014,11 +2017,11 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2014
2017
  // stays all-or-nothing and the count is the total actually inserted.
2015
2018
  const runs = createManyRunsOf(t);
2016
2019
  if (runs.length <= 1)
2017
- return qi.createMany(t).then((r) => ({ count: r.length }));
2020
+ return qi.createMany(t);
2018
2021
  return runInTx((table) => createManyByRun(table(mm.table), t, runs));
2019
2022
  }, {
2020
2023
  build: (qi, t) => qi.buildCreateMany(t),
2021
- reshape: (raw) => ({ count: raw.length }),
2024
+ reshape: (raw) => raw,
2022
2025
  // A mixed batch is more than one statement, so the array
2023
2026
  // $transaction([...]) form runs the whole array sequentially in a tx.
2024
2027
  nested: (t) => createManyRunsOf(t).length > 1,
@@ -54,6 +54,7 @@ exports.buildHavingNumericClauses = buildHavingNumericClauses;
54
54
  exports.buildAggregate = buildAggregate;
55
55
  const errors_js_1 = require("../errors.js");
56
56
  const schema_js_1 = require("../schema.js");
57
+ const expr_js_1 = require("./expr.js");
57
58
  const filters_js_1 = require("./filters.js");
58
59
  const types_js_1 = require("./types.js");
59
60
  const utils_js_1 = require("./utils.js");
@@ -325,7 +326,7 @@ function buildGroupBy(qi, args) {
325
326
  buildAggregates('_avg', 'AVG', args._avg);
326
327
  buildAggregates('_min', 'MIN', args._min);
327
328
  buildAggregates('_max', 'MAX', args._max);
328
- const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey);
329
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey, (key) => key === '_count' || usedResultKeys.has(key));
329
330
  selectExprs.push(...percentiles.map((plan) => plan.expr));
330
331
  let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
331
332
  // HAVING, filter whole groups by their aggregate values.
@@ -1070,7 +1071,7 @@ const PERCENTILE_HINT = {
1070
1071
  mssql: 'Percentile aggregates are not available on SQL Server in this release: PERCENTILE_CONT is a window ' +
1071
1072
  'function there, not an aggregate.',
1072
1073
  };
1073
- const PERCENTILE_SPEC_KEYS = ['p', 'method'];
1074
+ const PERCENTILE_SPEC_KEYS = ['p', 'method', 'of'];
1074
1075
  const PERCENTILE_METHODS = ['cont', 'disc'];
1075
1076
  const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is not supported yet.';
1076
1077
  /**
@@ -1085,8 +1086,14 @@ const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is
1085
1086
  * form), a JSON-path target, and two keys naming one column. Both methods need
1086
1087
  * `includePii: UNSAFE` on a PII column: `disc` returns a stored cell, and so
1087
1088
  * does `cont` at p = 0 or 1 or over a one-row group.
1089
+ *
1090
+ * A key with `of` names an expression's result (see `PercentileExprSpec`): it
1091
+ * must not name a field, the expression compiles through `compileReadExpr`,
1092
+ * it is ordered as `float8`, and its SQL alias is `_percentile_of_<n>`, so the
1093
+ * caller's name never reaches the SQL. Every PII column it reads as a value
1094
+ * needs `includePii: UNSAFE`, as a column percentile does.
1088
1095
  */
1089
- function planPercentiles(qi, spec, params, includePii, operation, claimResultKey) {
1096
+ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey, resultKeyTaken) {
1090
1097
  if (spec === undefined)
1091
1098
  return [];
1092
1099
  const where = `${operation} _percentile on table "${qi.table}"`;
@@ -1102,6 +1109,16 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1102
1109
  const meta = qi.schema.tables[qi.table];
1103
1110
  const plans = [];
1104
1111
  const aliases = new Set();
1112
+ // Column aliases are `_percentile_<column>`; an expression's is the first
1113
+ // free `_percentile_of_<n>`, chosen after every column alias is known so a
1114
+ // column named `of_0` keeps its own.
1115
+ const columnAliases = new Set();
1116
+ for (const [key] of entries) {
1117
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
1118
+ if (column !== undefined)
1119
+ columnAliases.add(`_percentile_${column}`);
1120
+ }
1121
+ let exprCount = 0;
1105
1122
  for (const [key, entry] of entries) {
1106
1123
  const label = `${where}, field "${key}"`;
1107
1124
  if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
@@ -1116,7 +1133,7 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1116
1133
  throw new errors_js_1.ValidationError(`${label}: unknown key "${k}".${hint ? ` Did you mean "${hint}"?` : ''} Expected { p, method? }.`);
1117
1134
  }
1118
1135
  }
1119
- const { p, method = 'cont' } = entry;
1136
+ const { p, method = 'cont', of } = entry;
1120
1137
  if (!PERCENTILE_METHODS.includes(method)) {
1121
1138
  throw new errors_js_1.ValidationError(`${label}: method must be 'cont' or 'disc', received ${describeValue(method)}.`);
1122
1139
  }
@@ -1129,6 +1146,10 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1129
1146
  else {
1130
1147
  assertFraction(p, label);
1131
1148
  }
1149
+ if (of !== undefined) {
1150
+ plans.push(planExpressionPercentile(qi, key, of, p, method, params, includePii, operation, label, aliases, columnAliases, exprCount++, claimResultKey, resultKeyTaken));
1151
+ continue;
1152
+ }
1132
1153
  const col = qi.toColumn(key);
1133
1154
  if (method === 'cont' && qi.tableMeta.dateColumns.has(col)) {
1134
1155
  throw new errors_js_1.ValidationError(`${label}: method 'cont' interpolates and needs a numeric or interval column, and "${col}" is a ` +
@@ -1158,6 +1179,51 @@ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey
1158
1179
  }
1159
1180
  return plans;
1160
1181
  }
1182
+ /** One `_percentile` entry over an expression (`of`); see {@link planPercentiles}. */
1183
+ function planExpressionPercentile(qi, key, of, p, method, params, includePii, operation, label, aliases, columnAliases, index, claimResultKey, resultKeyTaken) {
1184
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
1185
+ if (column !== undefined) {
1186
+ throw new errors_js_1.ValidationError(`${label}: "${key}" is a field of the table, and \`of\` names an expression. Give the expression a name ` +
1187
+ `that is not a field (the name is only its result's key), or drop \`of\` for the percentile of "${key}".`);
1188
+ }
1189
+ const compiled = (0, expr_js_1.compileReadExpr)({
1190
+ qi,
1191
+ name: key,
1192
+ operation: `${operation} _percentile`,
1193
+ bind: (value, cls) => {
1194
+ params.push(value);
1195
+ return `${qi.p(params.length)}::${cls === 'int' ? 'int8' : 'numeric'}`;
1196
+ },
1197
+ predicate: (when) => {
1198
+ const sql = whereMod.buildWhereClause(qi, when, params);
1199
+ if (sql === null)
1200
+ throw new errors_js_1.ValidationError(`${label}: an expr.case() arm's when compiles to no predicate.`);
1201
+ return sql;
1202
+ },
1203
+ }, of);
1204
+ const meta = qi.schema.tables[qi.table];
1205
+ for (const col of compiled.values) {
1206
+ const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
1207
+ assertAggregatePiiOptIn(qi.table, meta, field, col, `${operation} _percentile "${key}"`, includePii);
1208
+ }
1209
+ // The caller's key never names the alias, so it skips every name already
1210
+ // taken, a groupBy key included, rather than collide with one.
1211
+ let n = index;
1212
+ const taken = (alias) => aliases.has(alias) || columnAliases.has(alias) || resultKeyTaken?.(alias) === true;
1213
+ while (taken(`_percentile_of_${n}`))
1214
+ n++;
1215
+ const alias = `_percentile_of_${n}`;
1216
+ aliases.add(alias);
1217
+ claimResultKey?.(alias, `_percentile "${key}"`);
1218
+ params.push(Array.isArray(p) ? [...p] : p);
1219
+ const fractions = `${qi.p(params.length)}::float8${Array.isArray(p) ? '[]' : ''}`;
1220
+ return {
1221
+ expr: `percentile_${method}(${fractions}) WITHIN GROUP (ORDER BY (${compiled.sql})::float8) AS ${qi.q(alias)}`,
1222
+ alias,
1223
+ field: key,
1224
+ col: null,
1225
+ };
1226
+ }
1161
1227
  function assertFraction(value, label) {
1162
1228
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
1163
1229
  return;
@@ -1199,6 +1265,10 @@ function readPercentiles(qi, plans, row) {
1199
1265
  function percentileElement(qi, col, value) {
1200
1266
  if (value === null || value === undefined)
1201
1267
  return null;
1268
+ // An expression is ordered as float8, which the driver reads as a number
1269
+ // (a supplied pool's own parser may hand back text).
1270
+ if (col === null)
1271
+ return typeof value === 'string' ? Number(value) : value;
1202
1272
  if (typeof value === 'string' && isInt8Type(whereMod.getColumnPgType(qi, col))) {
1203
1273
  const n = Number(value);
1204
1274
  return Number.isSafeInteger(n) ? n : value;