turbine-orm 0.54.0 → 0.55.0
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 +3 -3
- package/dist/cjs/client.d.ts +85 -15
- package/dist/cjs/client.js +22 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +38 -4
- package/dist/cjs/query/builder.d.ts +56 -1
- package/dist/cjs/query/builder.js +145 -23
- package/dist/cjs/query/deferred.d.ts +35 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/utils.d.ts +83 -0
- package/dist/cjs/query/utils.js +174 -4
- package/dist/cjs/query/warn-registry.d.ts +14 -0
- package/dist/cjs/query/warn-registry.js +14 -0
- package/dist/cjs/query/where.d.ts +8 -0
- package/dist/client.d.ts +85 -15
- package/dist/client.js +23 -3
- package/dist/index.d.ts +1 -1
- package/dist/query/aggregates.js +39 -5
- package/dist/query/builder.d.ts +56 -1
- package/dist/query/builder.js +146 -24
- package/dist/query/deferred.d.ts +35 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/utils.d.ts +83 -0
- package/dist/query/utils.js +170 -4
- package/dist/query/warn-registry.d.ts +14 -0
- package/dist/query/warn-registry.js +14 -0
- package/dist/query/where.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -429,7 +429,7 @@ const db = turbine({
|
|
|
429
429
|
});
|
|
430
430
|
```
|
|
431
431
|
|
|
432
|
-
> **`utcTimestamps` is a process-wide decision, not a per-client one.** The two halves settle differently. The WRITE half is per client: a `Date` bound to a zone-less `date` / `timestamp` column is rewritten to a UTC literal unless that client opted out. The READ half is a set of pg type parsers on OIDs 1114 (`timestamp`), 1082 (`date`) and their array forms 1115 / 1182, and `pg.types.setTypeParser` installs one parser per OID for the whole pg module, shared by every pool, every raw query, and any other library on the same `pg`. The first client in the process settles it for all the rest. Constructing a second client with the **opposite** value therefore throws `ValidationError` (`TURBINE_E003`) at construction, rather than handing back a client that writes UTC and reads local (or the reverse) and so does not round-trip its own values. The message names both values and the two ways out: give every client in the process the same `utcTimestamps`, or run the odd one out in its own process. Clients built on an external pool (`pool: ...`, `turbineHttp()`) never REGISTER a parser and settle nothing on their own, so a process holding only those keeps whatever parser configuration the caller set up. They are not exempt from the check, though: registration is process-global, so once a Turbine-owned client has installed the parsers an external-pool client reads through them too, and a disagreeing one would write local `date` literals while reading UTC.
|
|
432
|
+
> **`utcTimestamps` is a process-wide decision, not a per-client one.** The two halves settle differently. The WRITE half is per client: a `Date` bound to a zone-less `date` / `timestamp` column is rewritten to a UTC literal unless that client opted out. The READ half is a set of pg type parsers on OIDs 1114 (`timestamp`), 1082 (`date`) and their array forms 1115 / 1182, and `pg.types.setTypeParser` installs one parser per OID for the whole pg module, shared by every pool, every raw query, and any other library on the same `pg`. It is also **retroactive**: there is one parser table and it is read per row at decode time, so registering changes pools **that already exist and are already querying**, not just pools created afterwards. The same `pg.Pool` running the same query returns different values before and after some unrelated module constructs a Turbine client, and with lazy route imports that ordering is not stable between requests. If the OID had already been customized by something else, Turbine emits a one-time dev warning rather than replacing it silently. The first client in the process settles it for all the rest. Constructing a second client with the **opposite** value therefore throws `ValidationError` (`TURBINE_E003`) at construction, rather than handing back a client that writes UTC and reads local (or the reverse) and so does not round-trip its own values. The message names both values and the two ways out: give every client in the process the same `utcTimestamps`, or run the odd one out in its own process. Clients built on an external pool (`pool: ...`, `turbineHttp()`) never REGISTER a parser and settle nothing on their own, so a process holding only those keeps whatever parser configuration the caller set up. They are not exempt from the check, though: registration is process-global, so once a Turbine-owned client has installed the parsers an external-pool client reads through them too, and a disagreeing one would write local `date` literals while reading UTC.
|
|
433
433
|
|
|
434
434
|
> **Upgrading with `utcTimestamps: false`.** If you already set `false`, this release changes the **stored text** of your writes: the write path now honors the flag where it previously ignored it, so zone-less columns receive local-calendar literals instead of UTC ones. Rows written before the upgrade and rows written after carry two conventions in the same column until you backfill.
|
|
435
435
|
|
|
@@ -454,7 +454,7 @@ const db = turbine({
|
|
|
454
454
|
|
|
455
455
|
Where a pg-style alias exists (`max`, `idleTimeoutMillis`, `connectionTimeoutMillis`), the explicit Turbine field wins when both are set.
|
|
456
456
|
|
|
457
|
-
> **`planCacheMode` (Postgres only, opt-in).** PostgreSQL
|
|
457
|
+
> **`planCacheMode` (Postgres only, opt-in).** PostgreSQL may promote a **named** prepared statement to a generic plan from its sixth execution onward, and a generic plan is costed blind to the bound values. On a predicate whose selectivity swings per value (a `tenant_id` equality on a shared table, where one value matches a handful of rows and another matches most of them), the statement can be locked onto a plan chosen for the average value, and it never reverts. `planCacheMode: 'auto' | 'force_custom_plan' | 'force_generic_plan'` pins the backend's choice; `'force_custom_plan'` re-plans every execution and removes the cliff. It is applied as a connection parameter (`options=-c plan_cache_mode=...`) when Turbine opens a connection, so it is in force for that connection's first statement and for every checkout, `$transaction`, stream and pipeline on it, and it cannot race your first query. Leave it unset (the default) and Turbine sends nothing at all. Reach for it when you have measured a statement getting slower after its fifth execution. **Correction to the 0.54 text, which said `findMany` / `findFirst` bind `LIMIT $n` and are "much less exposed":** that was false. PostgreSQL does not deny the planner a limit fraction for a bound limit, it substitutes a default of 10% of the child node's own row estimate (clamped at one row), and an unknown `OFFSET` triggers the same substitution even when the limit is a constant, which a paginated Turbine read always has. Two things also need saying about the sentence that opens this note. The sixth execution is a ceiling, not a trigger: `auto` promotes only when the generic plan's **estimated** cost is not worse than the average custom cost, so many statements are never promoted at all, and `pg_prepared_statements.generic_plans` is how you tell. And the shape that gets promoted unprompted is the one with **no limit**, not the limited one: measured on a skewed join predicate, an unlimited `count()`-shaped statement promoted under the default `auto` and ran a nested loop at 430x the buffers of the custom plan, while the same predicate under `LIMIT $n` was never promoted across eight executions (its substituted row count made the generic plan look more expensive). A limited `findMany` gives the planner two unknowns instead of one, which is not the same as more damage. `implicitPkOrdering` is **off by default in core**, so a default `findMany` emits no `ORDER BY`; switching it on adds an ordering a generic plan can walk the whole table in. Measure with `plan_cache_mode = force_generic_plan` against `force_custom_plan` rather than reasoning about which shapes ought to be safe; the fixtures and numbers are on the [relations page](https://turbineorm.dev/relations) and in the 0.55.0 changelog. Three scope limits: it does nothing on an **external pool** (Turbine never opens those connections, so set the GUC in the driver's own setup; Turbine-owned string `replicas` on that same client DO get it); a Postgres wire-compatible engine without the setting (CockroachDB, YugabyteDB, pre-12 PostgreSQL) refuses the connection parameter itself; and a **connection pooler** may filter startup parameters (PgBouncer's `ignore_startup_parameters`), where `ALTER ROLE ... SET plan_cache_mode = ...` is the way in. Any value outside the three throws `ValidationError` at construction, and a non-Postgres engine throws `UnsupportedFeatureError` (`TURBINE_E017`).
|
|
458
458
|
|
|
459
459
|
> **`preparedStatements` and connection poolers.** With prepared statements on, Turbine submits queries as `{ name, text, values }` so Postgres caches the parse and plan **per backend connection**. That is a real win against a database you connect to directly, and a hazard behind a transaction-pooling proxy (PgBouncer in `transaction` mode, Supabase's pooler port, some serverless poolers): the named statement is prepared on one backend and your next query may land on another, which fails with `prepared statement "..." does not exist`. Turbine defaults it to `true` only for pools it creates itself and `false` for external pools passed via `pool` / `turbineHttp()`, because serverless drivers are the common case there. If you are pointing a Turbine-owned pool at a transaction pooler, set `preparedStatements: false`. The environment variable `TURBINE_DISABLE_PREPARED=1` turns it off globally without a code change.
|
|
460
460
|
|
|
@@ -1097,7 +1097,7 @@ Turbine maps Postgres types to TypeScript:
|
|
|
1097
1097
|
| `int8` / `bigint` | `number` | Values > `Number.MAX_SAFE_INTEGER` (2^53 - 1) are returned as `string` at runtime to avoid precision loss. This affects < 0.01% of use cases (auto-increment IDs, counts, etc. are all safe). |
|
|
1098
1098
|
| `numeric`, `money` | `string` | Arbitrary precision, kept as string to avoid JS float issues |
|
|
1099
1099
|
| `text`, `varchar`, `uuid`, `citext` | `string` | |
|
|
1100
|
-
| `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) **and `date`** are parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. Since v0.52 the flag also reaches the WRITE side on Postgres: it governs the `Date` values that `create` / `update` / `upsert` and `where` clauses bind to zone-less `date` / `timestamp` columns. Before v0.52 it reached the read path only, so a client that had set `false` was still binding UTC, and those statements (and the text they store) change on upgrade. The two halves settle at different scopes: the write half is per client, the read half is a process-global pg type parser, so every client in one process must agree or construction throws `ValidationError`. Since v0.54 `date` reads at UTC midnight rather than the process's local midnight (its array form too), which moves the **epoch value** of a `date` by your process's UTC offset even though the calendar day it denotes is unchanged: format with `toISOString().slice(0, 10)`, not with local-component helpers like `toLocaleDateString()`. See [Relation loading and wire encoding](#relation-loading-and-wire-encoding). |
|
|
1100
|
+
| `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) **and `date`** are parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. Since v0.52 the flag also reaches the WRITE side on Postgres: it governs the `Date` values that `create` / `update` / `upsert` and `where` clauses bind to zone-less `date` / `timestamp` columns. Before v0.52 it reached the read path only, so a client that had set `false` was still binding UTC, and those statements (and the text they store) change on upgrade. The two halves settle at different scopes: the write half is per client, the read half is a process-global pg type parser, so every client in one process must agree or construction throws `ValidationError`. Since v0.54 `date` reads at UTC midnight rather than the process's local midnight (its array form too), which moves the **epoch value** of a `date` by your process's UTC offset even though the calendar day it denotes is unchanged: format with `toISOString().slice(0, 10)`, not with local-component helpers like `toLocaleDateString()`. Since v0.55 a stored `infinity` / `-infinity` reads as the JS number `Infinity` / `-Infinity` on **every** read strategy (v0.54 gave the number on the top-level and batched paths and an `Invalid Date` through a `with` clause, because `json_build_object` renders the value as the string `"infinity"` that no driver parser sees). There is no JS `Date` for either value, so the reading costs something either way and the default is the one that cannot **lose** a value: the number binds straight back, so `update({ data: { ...row } })` stores `infinity` again, whereas reading it as `null` makes a stored infinity indistinguishable from a stored NULL and that same write silently stores SQL NULL. The price of the default is that the field is declared `Date` and hands back a number, so `.toISOString()` / `.getTime()` throw a `TypeError` on those rows and `JSON.stringify` still renders them `null`. Set `temporalInfinity: 'null'` to read `null` instead, accepting the data loss, the collapse of `groupBy` / `distinct` keys, and `_max` returning `null` on a table with rows. Either way the value stays writable as `'infinity'` / `'-infinity'`, a one-time warning names the field the first time one is read (this one is NOT silenced by `NODE_ENV=production`, and naming either reading silences it), and `where: { col: null }` still means `IS NULL` and does not match those rows. See [Relation loading and wire encoding](#relation-loading-and-wire-encoding). |
|
|
1101
1101
|
| `boolean` | `boolean` | |
|
|
1102
1102
|
| `json`, `jsonb` | `unknown` | |
|
|
1103
1103
|
| `bytea` | `Buffer` | |
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { type Dialect } from './dialect.js';
|
|
|
26
26
|
import { type ErrorMessageMode } from './errors.js';
|
|
27
27
|
import { type ObserveConfig, type ObserveHandle } from './observe.js';
|
|
28
28
|
import { type PipelineOptions, type PipelineResults } from './pipeline.js';
|
|
29
|
-
import { type DeferredQuery, type GlobalFilters, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy } from './query/index.js';
|
|
29
|
+
import { type DeferredQuery, type GlobalFilters, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy, type TemporalInfinityReading } from './query/index.js';
|
|
30
30
|
import { type NotificationHandler, type Subscription } from './realtime.js';
|
|
31
31
|
import type { SchemaMetadata } from './schema.js';
|
|
32
32
|
import { TypedSqlQuery } from './typed-sql.js';
|
|
@@ -192,11 +192,21 @@ export interface TurbineConfig {
|
|
|
192
192
|
* rewrite (a bound `Date` on a zone-less `date` / `timestamp` column is then
|
|
193
193
|
* serialized by the driver in the process's zone).
|
|
194
194
|
*
|
|
195
|
-
* PER PROCESS, NOT PER CLIENT. The read half is a pg type
|
|
196
|
-
* `pg.types.setTypeParser` installs one parser per OID for the
|
|
197
|
-
* process.
|
|
198
|
-
*
|
|
199
|
-
*
|
|
195
|
+
* PER PROCESS, NOT PER CLIENT, AND RETROACTIVE. The read half is a pg type
|
|
196
|
+
* parser, and `pg.types.setTypeParser` installs one parser per OID for the
|
|
197
|
+
* whole process. There is ONE parser table and it is consulted per row at
|
|
198
|
+
* decode time, so registration also changes POOLS THAT ALREADY EXIST AND ARE
|
|
199
|
+
* ALREADY QUERYING: the same `pg.Pool` running the same query returns
|
|
200
|
+
* different values before and after a Turbine client is constructed
|
|
201
|
+
* somewhere else in the process. With lazy route imports that ordering is
|
|
202
|
+
* not stable between requests, so a reporting job on its own pool can return
|
|
203
|
+
* different days depending on what has been imported yet. When the OID was
|
|
204
|
+
* still on the driver's default that is the intended trade; when something
|
|
205
|
+
* else had already customized it, Turbine says so once (dev-only, see
|
|
206
|
+
* `warnParserOverwrite`). The first client settles it for every later one, so
|
|
207
|
+
* constructing a second client with the OPPOSITE value throws a
|
|
208
|
+
* `ValidationError` rather than handing back a client whose writes and reads
|
|
209
|
+
* disagree. Give every
|
|
200
210
|
* client in the process the same value, or isolate the odd one in its own
|
|
201
211
|
* process. A client on an EXTERNAL pool never REGISTERS a parser (it inherits
|
|
202
212
|
* whatever configuration the caller's driver has, so a process containing
|
|
@@ -207,27 +217,80 @@ export interface TurbineConfig {
|
|
|
207
217
|
* UTC.
|
|
208
218
|
*/
|
|
209
219
|
utcTimestamps?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
222
|
+
* `'preserve'` (the default, the JS numbers `Infinity` / `-Infinity`) or
|
|
223
|
+
* `'null'`. See {@link TemporalInfinityReading} for the full trade.
|
|
224
|
+
*
|
|
225
|
+
* There is no JS `Date` for either value, so both readings are wrong in some
|
|
226
|
+
* way and the option is which way. `'preserve'` is LOSSLESS: binding the
|
|
227
|
+
* number back stores `infinity` again, so a read-modify-write
|
|
228
|
+
* (`update({ data: { ...row } })`) round-trips. Its cost is a number on a
|
|
229
|
+
* `Date`-typed field, so `row.validUntil.toISOString()` throws a TypeError on
|
|
230
|
+
* exactly those rows, and `JSON.stringify` still renders the value `null`.
|
|
231
|
+
* `'null'` matches what `JSON.stringify` already produced and keeps the
|
|
232
|
+
* field's declared `Date | null` type honest, at the cost of DATA LOSS: a
|
|
233
|
+
* stored `infinity` and a stored NULL become indistinguishable, so that same
|
|
234
|
+
* read-modify-write writes SQL NULL and destroys the value with no error.
|
|
235
|
+
* The default is the reading that cannot lose a value.
|
|
236
|
+
*
|
|
237
|
+
* Unlike 0.54, which returned the number on some read strategies and an
|
|
238
|
+
* Invalid Date on others, BOTH readings here are identical on every
|
|
239
|
+
* strategy, every write projection, `groupBy` keys and `_min` / `_max`.
|
|
240
|
+
*
|
|
241
|
+
* Leaving the option unset selects `'preserve'` AND enables a one-time
|
|
242
|
+
* warning (per process, per field, not silenced by `NODE_ENV=production`) the
|
|
243
|
+
* first time a stored infinity is actually read, describing that reading and
|
|
244
|
+
* both escapes. Naming either reading explicitly silences it.
|
|
245
|
+
*/
|
|
246
|
+
temporalInfinity?: TemporalInfinityReading;
|
|
210
247
|
/**
|
|
211
248
|
* Pin `plan_cache_mode` on every connection this client opens, fixing how the
|
|
212
249
|
* PostgreSQL backend chooses between a custom plan (re-planned per parameter
|
|
213
250
|
* set) and a generic plan (planned once, blind to the values).
|
|
214
251
|
*
|
|
215
252
|
* Why it exists: Turbine sends NAMED prepared statements by default on a pool
|
|
216
|
-
* it owns, and PostgreSQL
|
|
217
|
-
* five executions
|
|
253
|
+
* it owns, and PostgreSQL MAY promote a named statement to a generic plan
|
|
254
|
+
* after five executions (see the ceiling note below: it promotes only when
|
|
255
|
+
* the generic plan's estimated cost is not worse than the average custom
|
|
256
|
+
* cost). A generic plan is planned for the AVERAGE parameter, so a
|
|
218
257
|
* predicate whose selectivity varies wildly per value (the canonical case is a
|
|
219
258
|
* `tenant_id` / `user_id` equality on a shared table, where one value matches
|
|
220
259
|
* a handful of rows and another matches most of them) is planned blind to the
|
|
221
260
|
* value it will actually get, and it never reverts. `'force_custom_plan'`
|
|
222
261
|
* removes that, at the cost of re-planning each execution.
|
|
223
262
|
*
|
|
224
|
-
* Scope
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* `LIMIT $n
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
263
|
+
* Scope. It applies to the statements Turbine itself promotes, which is any
|
|
264
|
+
* of them: `count()`, `findMany` and `findFirst` alike. CORRECTION TO THE
|
|
265
|
+
* 0.54.0 TEXT, which claimed `findMany` / `findFirst` were "much less
|
|
266
|
+
* exposed" because they bind `LIMIT $n`: that was false. PostgreSQL does not
|
|
267
|
+
* deny the planner a limit fraction for a bound limit, it SUBSTITUTES a
|
|
268
|
+
* default of 10% of the child node's own row estimate (clamped at one row),
|
|
269
|
+
* which is simply a different wrong number. An unknown `OFFSET` triggers the
|
|
270
|
+
* same substitution on its own even when the limit is a constant, and a
|
|
271
|
+
* paginated Turbine read binds both.
|
|
272
|
+
*
|
|
273
|
+
* Two things the sentence above this one glosses over. The sixth execution
|
|
274
|
+
* is a CEILING, not a trigger: `auto` promotes only when the generic plan's
|
|
275
|
+
* ESTIMATED cost is not worse than the average custom cost, so plenty of
|
|
276
|
+
* statements are never promoted at all (`pg_prepared_statements.generic_plans`
|
|
277
|
+
* is how you tell). And the shape that gets promoted unprompted is the one
|
|
278
|
+
* with NO limit: measured on a skewed join predicate, the unlimited statement
|
|
279
|
+
* promoted under the default `auto` and ran a nested loop at 430x the buffers
|
|
280
|
+
* of the custom plan, while the same predicate under `LIMIT $n` was never
|
|
281
|
+
* promoted across eight executions, because its substituted row count made
|
|
282
|
+
* the generic plan look MORE expensive. A limited `findMany` gives the
|
|
283
|
+
* planner two unknowns instead of one, which is not the same thing as more
|
|
284
|
+
* damage.
|
|
285
|
+
*
|
|
286
|
+
* `implicitPkOrdering` is OFF by default in core, so a default `findMany`
|
|
287
|
+
* emits no `ORDER BY` at all; switching it on adds an ordering a generic plan
|
|
288
|
+
* can walk the whole table in.
|
|
289
|
+
*
|
|
290
|
+
* Treat the option as a targeted remedy for a plan that is measurably worse
|
|
291
|
+
* after its fifth execution, not a general speed-up, and measure with
|
|
292
|
+
* `plan_cache_mode = force_generic_plan` versus `force_custom_plan` rather
|
|
293
|
+
* than reasoning about which query shapes "should" be safe.
|
|
231
294
|
*
|
|
232
295
|
* Default `undefined`: Turbine issues NOTHING and the backend keeps its own
|
|
233
296
|
* default (`auto`), byte-identical to not setting the option.
|
|
@@ -689,6 +752,13 @@ export declare class TurbineClient {
|
|
|
689
752
|
* `UnsupportedFeatureError` (E017), in the same style as the other
|
|
690
753
|
* capability refusals.
|
|
691
754
|
*/
|
|
755
|
+
/**
|
|
756
|
+
* Validate the `temporalInfinity` reading. A closed two-value enum, checked
|
|
757
|
+
* at construction so a typo (`'preserved'`, `'raw'`) fails loudly rather than
|
|
758
|
+
* silently falling back to the default reading the caller was trying to
|
|
759
|
+
* change.
|
|
760
|
+
*/
|
|
761
|
+
private static resolveTemporalInfinity;
|
|
692
762
|
private static resolvePlanCacheMode;
|
|
693
763
|
/**
|
|
694
764
|
* Pin `plan_cache_mode` on every connection an OWNED pool opens, by putting
|
package/dist/cjs/client.js
CHANGED
|
@@ -109,6 +109,7 @@ const TURBINE_CONFIG_KEYS = {
|
|
|
109
109
|
defaultLimit: true,
|
|
110
110
|
warnOnUnlimited: true,
|
|
111
111
|
utcTimestamps: true,
|
|
112
|
+
temporalInfinity: true,
|
|
112
113
|
planCacheMode: true,
|
|
113
114
|
scopedConnect: true,
|
|
114
115
|
relationLoadStrategy: true,
|
|
@@ -599,10 +600,11 @@ class TurbineClient {
|
|
|
599
600
|
// constructor-gated by the static flags, so it happens at most once.
|
|
600
601
|
const ownsAnyPool = !config.pool;
|
|
601
602
|
if (ownsAnyPool && !TurbineClient.int8ParserRegistered) {
|
|
602
|
-
|
|
603
|
+
(0, utils_js_1.warnParserOverwrite)(20, 'int8');
|
|
604
|
+
pg_1.default.types.setTypeParser(20, (0, utils_js_1.markTurbineParser)((val) => {
|
|
603
605
|
const n = Number(val);
|
|
604
606
|
return Number.isSafeInteger(n) ? n : val;
|
|
605
|
-
});
|
|
607
|
+
}));
|
|
606
608
|
TurbineClient.int8ParserRegistered = true;
|
|
607
609
|
}
|
|
608
610
|
// Parse the zone-less temporal types (`timestamp` OID 1114, `date` OID
|
|
@@ -646,6 +648,7 @@ class TurbineClient {
|
|
|
646
648
|
defaultLimit: config.defaultLimit,
|
|
647
649
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
648
650
|
utcTimestamps: config.utcTimestamps,
|
|
651
|
+
temporalInfinity: TurbineClient.resolveTemporalInfinity(config.temporalInfinity),
|
|
649
652
|
scopedConnect: config.scopedConnect,
|
|
650
653
|
relationLoadStrategy: config.relationLoadStrategy,
|
|
651
654
|
stableRelationOrder: config.stableRelationOrder,
|
|
@@ -817,6 +820,23 @@ class TurbineClient {
|
|
|
817
820
|
* `UnsupportedFeatureError` (E017), in the same style as the other
|
|
818
821
|
* capability refusals.
|
|
819
822
|
*/
|
|
823
|
+
/**
|
|
824
|
+
* Validate the `temporalInfinity` reading. A closed two-value enum, checked
|
|
825
|
+
* at construction so a typo (`'preserved'`, `'raw'`) fails loudly rather than
|
|
826
|
+
* silently falling back to the default reading the caller was trying to
|
|
827
|
+
* change.
|
|
828
|
+
*/
|
|
829
|
+
static resolveTemporalInfinity(value) {
|
|
830
|
+
if (value === undefined)
|
|
831
|
+
return undefined;
|
|
832
|
+
if (value !== 'null' && value !== 'preserve') {
|
|
833
|
+
throw new errors_js_1.ValidationError(`Invalid temporalInfinity: ${JSON.stringify(value)}. Expected 'preserve' (default: read a Postgres ` +
|
|
834
|
+
'temporal `infinity` as the JS number `Infinity` / `-Infinity`, which round-trips through a write ' +
|
|
835
|
+
"but breaks the declared `Date` type) or 'null' (read it as null, which serializes cleanly but " +
|
|
836
|
+
'makes it indistinguishable from a stored NULL, so a read-modify-write destroys the value).');
|
|
837
|
+
}
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
820
840
|
static resolvePlanCacheMode(mode, dialect) {
|
|
821
841
|
if (mode === undefined || mode === null)
|
|
822
842
|
return undefined;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
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 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 JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, 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';
|
|
46
|
+
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 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 JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, 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';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
|
@@ -353,12 +353,14 @@ function buildGroupBy(qi, args) {
|
|
|
353
353
|
}
|
|
354
354
|
else if (rawKey.startsWith('_min_')) {
|
|
355
355
|
const j = jsonAgg(rawKey);
|
|
356
|
-
minObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
356
|
+
minObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
357
|
+
j?.numeric && rawValue !== null ? Number(rawValue) : temporalAggValue(qi, rawKey.slice(5), rawValue);
|
|
357
358
|
hasMins = true;
|
|
358
359
|
}
|
|
359
360
|
else if (rawKey.startsWith('_max_')) {
|
|
360
361
|
const j = jsonAgg(rawKey);
|
|
361
|
-
maxObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
362
|
+
maxObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
363
|
+
j?.numeric && rawValue !== null ? Number(rawValue) : temporalAggValue(qi, rawKey.slice(5), rawValue);
|
|
362
364
|
hasMaxs = true;
|
|
363
365
|
}
|
|
364
366
|
}
|
|
@@ -768,6 +770,38 @@ function buildHavingNumericClauses(qi, expr, filter, params) {
|
|
|
768
770
|
}
|
|
769
771
|
return clauses;
|
|
770
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* `_min` / `_max` over a TEMPORAL column, aligned with the row parser.
|
|
775
|
+
*
|
|
776
|
+
* These two are the only aggregates that hand back a row's stored cell rather
|
|
777
|
+
* than something computed across rows, so they are the only ones that can carry
|
|
778
|
+
* a Postgres `infinity`. They are assembled from the RAW row (a Date must stay
|
|
779
|
+
* a Date, and `parseRow`'s snake→camel mapping would collide with the `_min_`
|
|
780
|
+
* alias), so the infinity mapping has to be applied here too. Without it
|
|
781
|
+
* `aggregate({ _max: { ts: true } })` would return the driver's `Infinity`
|
|
782
|
+
* while `findMany` returned `null` for the very same value.
|
|
783
|
+
*
|
|
784
|
+
* Gated on the column being temporal, so a numeric `_max` is untouched, and on
|
|
785
|
+
* the client's `temporalInfinity` reading, so it cannot disagree with the rows
|
|
786
|
+
* `findMany` returns for the same column. An absent reading resolves to the
|
|
787
|
+
* default, `'preserve'`, exactly as it does in the row parser.
|
|
788
|
+
*
|
|
789
|
+
* Note what the opt-in `'null'` reading then means for `_max` on a table that
|
|
790
|
+
* plainly has rows: `null` is the same value an empty table and an all-NULL
|
|
791
|
+
* column return, so `_min` can report a real date while `_max` reports
|
|
792
|
+
* "nothing" (documented, and one of the reasons `'null'` is not the default).
|
|
793
|
+
*/
|
|
794
|
+
function temporalAggValue(qi, col, value) {
|
|
795
|
+
if (!qi.tableMeta.dateColumns.has(col))
|
|
796
|
+
return value;
|
|
797
|
+
if (!(0, utils_js_1.isTemporalInfinity)(value))
|
|
798
|
+
return value;
|
|
799
|
+
if (qi.temporalInfinity === 'null')
|
|
800
|
+
return null;
|
|
801
|
+
if (typeof value === 'number')
|
|
802
|
+
return value;
|
|
803
|
+
return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
804
|
+
}
|
|
771
805
|
function buildAggregate(qi, args) {
|
|
772
806
|
qi.currentSkip = args.skipGlobalFilters;
|
|
773
807
|
const aggWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
@@ -912,13 +946,13 @@ function buildAggregate(qi, args) {
|
|
|
912
946
|
else if (key.startsWith('_min_')) {
|
|
913
947
|
const col = key.slice(5);
|
|
914
948
|
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
915
|
-
minObj[field] = val;
|
|
949
|
+
minObj[field] = temporalAggValue(qi, col, val);
|
|
916
950
|
hasMins = true;
|
|
917
951
|
}
|
|
918
952
|
else if (key.startsWith('_max_')) {
|
|
919
953
|
const col = key.slice(5);
|
|
920
954
|
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
921
|
-
maxObj[field] = val;
|
|
955
|
+
maxObj[field] = temporalAggValue(qi, col, val);
|
|
922
956
|
hasMaxs = true;
|
|
923
957
|
}
|
|
924
958
|
}
|
|
@@ -154,7 +154,7 @@ export declare const AUTO_TO_ONE_JOIN_ROWS_MAX = 100000;
|
|
|
154
154
|
* it.
|
|
155
155
|
*/
|
|
156
156
|
export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
157
|
-
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
|
|
157
|
+
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './deferred.js';
|
|
158
158
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
159
159
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
160
160
|
private readonly pool;
|
|
@@ -181,6 +181,14 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
181
181
|
private readonly warnOnUnlimited;
|
|
182
182
|
private readonly scopedConnect;
|
|
183
183
|
private readonly utcTimestamps;
|
|
184
|
+
/**
|
|
185
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
186
|
+
* `'preserve'` (default) or `'null'`. See {@link TemporalInfinityReading}
|
|
187
|
+
* for the trade, and {@link parseRow} for where it is applied.
|
|
188
|
+
*/
|
|
189
|
+
private readonly temporalInfinity;
|
|
190
|
+
/** Whether `temporalInfinity` was left unset, i.e. the warning still applies. */
|
|
191
|
+
private readonly warnTemporalInfinityUnset;
|
|
184
192
|
private readonly preparedStatementsEnabled;
|
|
185
193
|
/**
|
|
186
194
|
* Whether the SQL template cache is active. Set once in the constructor.
|
|
@@ -1010,5 +1018,52 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1010
1018
|
* memoized per table. Used so nested relation rows (camelCase keys) coerce
|
|
1011
1019
|
* dates the same way top-level rows do.
|
|
1012
1020
|
*/
|
|
1021
|
+
/**
|
|
1022
|
+
* Say ONCE per `table.field` that a stored temporal `infinity` was actually
|
|
1023
|
+
* read, and describe the reading the caller is getting. JavaScript has no
|
|
1024
|
+
* `Date` for either infinity, so BOTH readings cost something and a caller
|
|
1025
|
+
* whose rows carry the value needs to know which cost they are paying.
|
|
1026
|
+
*
|
|
1027
|
+
* Under the default `'preserve'` the value comes back as the JS number
|
|
1028
|
+
* `Infinity` / `-Infinity` on a field the generated types declare as `Date`,
|
|
1029
|
+
* so `.toISOString()` / `.getTime()` throw a TypeError on exactly those rows
|
|
1030
|
+
* and `JSON.stringify` still renders them `null` (JSON has no infinity
|
|
1031
|
+
* literal). That is the price of the reading being LOSSLESS: the number binds
|
|
1032
|
+
* straight back, so a read-modify-write stores `infinity` again. The
|
|
1033
|
+
* alternative, `'null'`, is silently destructive, which is why it is not the
|
|
1034
|
+
* default and why the warning names it as a deliberate choice rather than a
|
|
1035
|
+
* recommendation.
|
|
1036
|
+
*
|
|
1037
|
+
* ONLY WHEN THE OPTION WAS LEFT UNSET. Naming a reading in the config, either
|
|
1038
|
+
* one, is the acknowledgement, and the warning exists to surface an
|
|
1039
|
+
* unacknowledged trade rather than to nag.
|
|
1040
|
+
*
|
|
1041
|
+
* NOT DEV-ONLY, unlike every other warning in this codebase, and deliberately
|
|
1042
|
+
* so. Production is exactly where a destructive write commits and where the
|
|
1043
|
+
* row cannot be recovered afterwards, so a warning that goes quiet under
|
|
1044
|
+
* `NODE_ENV=production` is silent in the only place it matters. The cost is
|
|
1045
|
+
* bounded to the point of irrelevance: once per process per field, and only
|
|
1046
|
+
* on a row that actually held an infinity.
|
|
1047
|
+
*
|
|
1048
|
+
* KEYED ON THE FIELD, not the row key: top-level rows arrive snake_case and
|
|
1049
|
+
* nested `json_build_object` rows arrive camelCase, so keying on the raw key
|
|
1050
|
+
* warned twice for one column.
|
|
1051
|
+
*/
|
|
1052
|
+
private warnTemporalInfinity;
|
|
1053
|
+
/**
|
|
1054
|
+
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
1055
|
+
* value, returning the original array by identity when there are none (the
|
|
1056
|
+
* overwhelmingly common case, so no per-row allocation on ordinary data).
|
|
1057
|
+
*/
|
|
1058
|
+
private mapArrayTemporalInfinity;
|
|
1059
|
+
/**
|
|
1060
|
+
* The configured reading of one infinity value: the JS number (`'preserve'`,
|
|
1061
|
+
* the default) or `null`. Under `'preserve'` the JSON-wire STRING form
|
|
1062
|
+
* (`"infinity"`, what the join and positional strategies see) is normalized
|
|
1063
|
+
* to the number too, so the reading is identical on every strategy; 0.54
|
|
1064
|
+
* shipped the number on some paths and an Invalid Date on others, which is
|
|
1065
|
+
* the bug that made a single reading necessary in the first place.
|
|
1066
|
+
*/
|
|
1067
|
+
private readTemporalInfinity;
|
|
1013
1068
|
private parseRow;
|
|
1014
1069
|
}
|
|
@@ -377,6 +377,14 @@ class QueryInterface {
|
|
|
377
377
|
warnOnUnlimited;
|
|
378
378
|
scopedConnect;
|
|
379
379
|
utcTimestamps;
|
|
380
|
+
/**
|
|
381
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
382
|
+
* `'preserve'` (default) or `'null'`. See {@link TemporalInfinityReading}
|
|
383
|
+
* for the trade, and {@link parseRow} for where it is applied.
|
|
384
|
+
*/
|
|
385
|
+
temporalInfinity;
|
|
386
|
+
/** Whether `temporalInfinity` was left unset, i.e. the warning still applies. */
|
|
387
|
+
warnTemporalInfinityUnset;
|
|
380
388
|
preparedStatementsEnabled;
|
|
381
389
|
/**
|
|
382
390
|
* Whether the SQL template cache is active. Set once in the constructor.
|
|
@@ -523,6 +531,17 @@ class QueryInterface {
|
|
|
523
531
|
: warnOpt !== false;
|
|
524
532
|
this.scopedConnect = options?.scopedConnect === true;
|
|
525
533
|
this.utcTimestamps = options?.utcTimestamps !== false;
|
|
534
|
+
// Unset resolves to `'preserve'`. `'null'` destroys data: it makes a stored
|
|
535
|
+
// infinity indistinguishable from a stored NULL, so the ordinary
|
|
536
|
+
// read-modify-write (`update({ data: { ...row } })`) stores SQL NULL over a
|
|
537
|
+
// nullable temporal column and the infinity is gone with no error, measured
|
|
538
|
+
// on a live server. A lossy default is the wrong price for a nicer
|
|
539
|
+
// `JSON.stringify`, so the lossless reading is the default and `'null'`
|
|
540
|
+
// stays available as an explicit opt-in.
|
|
541
|
+
this.temporalInfinity = options?.temporalInfinity === 'null' ? 'null' : 'preserve';
|
|
542
|
+
// The warning exists to surface an UNACKNOWLEDGED trade. Naming either
|
|
543
|
+
// reading in the config is the acknowledgement, so it stops.
|
|
544
|
+
this.warnTemporalInfinityUnset = options?.temporalInfinity === undefined;
|
|
526
545
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
527
546
|
// SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
|
|
528
547
|
// (mirrors `sqlCache: false`); any positive integer sets the LRU bound;
|
|
@@ -619,6 +638,9 @@ class QueryInterface {
|
|
|
619
638
|
// building a client whose writes and reads disagree (see
|
|
620
639
|
// `assertUtcTimestampsAgree` in client.ts).
|
|
621
640
|
utcTimestamps: this.utcTimestamps,
|
|
641
|
+
// Reaches aggregates.ts, where `_min` / `_max` are assembled from the raw
|
|
642
|
+
// row and so need the same infinity reading as `parseRow`.
|
|
643
|
+
temporalInfinity: this.temporalInfinity,
|
|
622
644
|
crossSchemaTypeColumns: this.crossSchemaTypeColumns,
|
|
623
645
|
get currentSkip() {
|
|
624
646
|
return self.currentSkip;
|
|
@@ -3055,6 +3077,87 @@ class QueryInterface {
|
|
|
3055
3077
|
* memoized per table. Used so nested relation rows (camelCase keys) coerce
|
|
3056
3078
|
* dates the same way top-level rows do.
|
|
3057
3079
|
*/
|
|
3080
|
+
/**
|
|
3081
|
+
* Say ONCE per `table.field` that a stored temporal `infinity` was actually
|
|
3082
|
+
* read, and describe the reading the caller is getting. JavaScript has no
|
|
3083
|
+
* `Date` for either infinity, so BOTH readings cost something and a caller
|
|
3084
|
+
* whose rows carry the value needs to know which cost they are paying.
|
|
3085
|
+
*
|
|
3086
|
+
* Under the default `'preserve'` the value comes back as the JS number
|
|
3087
|
+
* `Infinity` / `-Infinity` on a field the generated types declare as `Date`,
|
|
3088
|
+
* so `.toISOString()` / `.getTime()` throw a TypeError on exactly those rows
|
|
3089
|
+
* and `JSON.stringify` still renders them `null` (JSON has no infinity
|
|
3090
|
+
* literal). That is the price of the reading being LOSSLESS: the number binds
|
|
3091
|
+
* straight back, so a read-modify-write stores `infinity` again. The
|
|
3092
|
+
* alternative, `'null'`, is silently destructive, which is why it is not the
|
|
3093
|
+
* default and why the warning names it as a deliberate choice rather than a
|
|
3094
|
+
* recommendation.
|
|
3095
|
+
*
|
|
3096
|
+
* ONLY WHEN THE OPTION WAS LEFT UNSET. Naming a reading in the config, either
|
|
3097
|
+
* one, is the acknowledgement, and the warning exists to surface an
|
|
3098
|
+
* unacknowledged trade rather than to nag.
|
|
3099
|
+
*
|
|
3100
|
+
* NOT DEV-ONLY, unlike every other warning in this codebase, and deliberately
|
|
3101
|
+
* so. Production is exactly where a destructive write commits and where the
|
|
3102
|
+
* row cannot be recovered afterwards, so a warning that goes quiet under
|
|
3103
|
+
* `NODE_ENV=production` is silent in the only place it matters. The cost is
|
|
3104
|
+
* bounded to the point of irrelevance: once per process per field, and only
|
|
3105
|
+
* on a row that actually held an infinity.
|
|
3106
|
+
*
|
|
3107
|
+
* KEYED ON THE FIELD, not the row key: top-level rows arrive snake_case and
|
|
3108
|
+
* nested `json_build_object` rows arrive camelCase, so keying on the raw key
|
|
3109
|
+
* warned twice for one column.
|
|
3110
|
+
*/
|
|
3111
|
+
warnTemporalInfinity(table, field) {
|
|
3112
|
+
if (!this.warnTemporalInfinityUnset)
|
|
3113
|
+
return;
|
|
3114
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.temporalInfinity, `${table}.${field}`))
|
|
3115
|
+
return;
|
|
3116
|
+
console.warn(`[turbine] ${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
|
|
3117
|
+
'no Date for it, so it reads as the JS number `Infinity` / `-Infinity` on a field the generated ' +
|
|
3118
|
+
'types declare as `Date`: `.toISOString()` and `.getTime()` throw a TypeError on these rows, and ' +
|
|
3119
|
+
'`JSON.stringify` still renders them null because JSON has no infinity literal. The number is the ' +
|
|
3120
|
+
'lossless reading, though: it binds straight back, so writing a row you just read stores `infinity` ' +
|
|
3121
|
+
'again. Two more things this column can no longer do: `where: { col: null }` still means IS NULL and ' +
|
|
3122
|
+
"does NOT match these rows (filter them with `{ col: 'infinity' }`), and `groupBy` / `distinct` " +
|
|
3123
|
+
"cannot tell infinity, -infinity and NULL apart. Set `temporalInfinity: 'preserve'` to confirm this " +
|
|
3124
|
+
"reading and silence the warning, or `'null'` to read null instead, accepting that a stored " +
|
|
3125
|
+
'infinity then looks exactly like a stored NULL and a read-modify-write over a nullable column ' +
|
|
3126
|
+
'stores SQL NULL, destroying the value with no error.');
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
3130
|
+
* value, returning the original array by identity when there are none (the
|
|
3131
|
+
* overwhelmingly common case, so no per-row allocation on ordinary data).
|
|
3132
|
+
*/
|
|
3133
|
+
mapArrayTemporalInfinity(value, table, field) {
|
|
3134
|
+
let hit = false;
|
|
3135
|
+
for (let i = 0; i < value.length; i++) {
|
|
3136
|
+
if ((0, utils_js_1.isTemporalInfinity)(value[i])) {
|
|
3137
|
+
hit = true;
|
|
3138
|
+
break;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (!hit)
|
|
3142
|
+
return value;
|
|
3143
|
+
this.warnTemporalInfinity(table, field);
|
|
3144
|
+
return value.map((el) => ((0, utils_js_1.isTemporalInfinity)(el) ? this.readTemporalInfinity(el) : el));
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* The configured reading of one infinity value: the JS number (`'preserve'`,
|
|
3148
|
+
* the default) or `null`. Under `'preserve'` the JSON-wire STRING form
|
|
3149
|
+
* (`"infinity"`, what the join and positional strategies see) is normalized
|
|
3150
|
+
* to the number too, so the reading is identical on every strategy; 0.54
|
|
3151
|
+
* shipped the number on some paths and an Invalid Date on others, which is
|
|
3152
|
+
* the bug that made a single reading necessary in the first place.
|
|
3153
|
+
*/
|
|
3154
|
+
readTemporalInfinity(value) {
|
|
3155
|
+
if (this.temporalInfinity === 'null')
|
|
3156
|
+
return null;
|
|
3157
|
+
if (typeof value === 'number')
|
|
3158
|
+
return value;
|
|
3159
|
+
return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
3160
|
+
}
|
|
3058
3161
|
parseRow(row, table) {
|
|
3059
3162
|
const parsed = {};
|
|
3060
3163
|
const meta = this.schema.tables[table];
|
|
@@ -3071,29 +3174,48 @@ class QueryInterface {
|
|
|
3071
3174
|
const value = row[col];
|
|
3072
3175
|
const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
|
|
3073
3176
|
// Top-level rows are snake_case (dateCols); nested rows are camelCase (camelDateFields).
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3177
|
+
if ((dateCols.has(col) || camelDateFields.has(field)) && value !== null && !(value instanceof Date)) {
|
|
3178
|
+
if ((0, utils_js_1.isTemporalInfinity)(value)) {
|
|
3179
|
+
// Postgres `infinity` / `-infinity`. No JS Date means either, so
|
|
3180
|
+
// both readings cost something and the default is the one that is
|
|
3181
|
+
// not lossy. `'preserve'` hands back the JS number, which breaks
|
|
3182
|
+
// the declared `Date` type at runtime (`.toISOString()` throws) and
|
|
3183
|
+
// still serializes as null because JSON has no infinity literal,
|
|
3184
|
+
// but binds straight back, so a read-modify-write stores `infinity`
|
|
3185
|
+
// again. `'null'` reads nicer and DESTROYS the value on that same
|
|
3186
|
+
// write, because a stored infinity and a stored NULL become
|
|
3187
|
+
// indistinguishable. Whichever is configured, it is the SAME on
|
|
3188
|
+
// every read strategy: the driver hands back the number,
|
|
3189
|
+
// `json_build_object` hands back the string "infinity", and both
|
|
3190
|
+
// land here (see `isTemporalInfinity`).
|
|
3191
|
+
//
|
|
3192
|
+
// The warning below fires once per column when the option was left
|
|
3193
|
+
// unset, on a row that actually held an infinity, and describes the
|
|
3194
|
+
// reading in force rather than gating on which one it is.
|
|
3195
|
+
this.warnTemporalInfinity(table, field);
|
|
3196
|
+
parsed[field] = this.readTemporalInfinity(value);
|
|
3197
|
+
}
|
|
3198
|
+
else if (Array.isArray(value)) {
|
|
3199
|
+
// `dateColumns` includes array-of-date columns (`date[]`,
|
|
3200
|
+
// `timestamp[]`, `timestamptz[]`), for which the driver already
|
|
3201
|
+
// hands back a `Date[]`. Coercing the array itself ran
|
|
3202
|
+
// `new Date(String(theArray))` and replaced the whole column with
|
|
3203
|
+
// one Invalid Date. Its ELEMENTS get the same infinity mapping as a
|
|
3204
|
+
// scalar (same declared element type, same JSON rendering);
|
|
3205
|
+
// everything else is passed through by identity.
|
|
3206
|
+
parsed[field] = this.mapArrayTemporalInfinity(value, table, field);
|
|
3207
|
+
}
|
|
3208
|
+
else if (typeof value === 'number') {
|
|
3209
|
+
// Any other number on a date column is left alone rather than run
|
|
3210
|
+
// through `parseDbDate(String(n))`, which would produce an Invalid
|
|
3211
|
+
// Date.
|
|
3212
|
+
parsed[field] = value;
|
|
3213
|
+
}
|
|
3214
|
+
else {
|
|
3215
|
+
// Offset-less strings (Postgres `timestamp`, json_agg output) are
|
|
3216
|
+
// pinned to UTC so results don't depend on the server's time zone.
|
|
3217
|
+
parsed[field] = this.utcTimestamps ? (0, utils_js_1.parseDbDate)(String(value)) : new Date(value);
|
|
3218
|
+
}
|
|
3097
3219
|
}
|
|
3098
3220
|
else {
|
|
3099
3221
|
parsed[field] = value;
|