turbine-orm 0.53.0 → 0.54.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.
@@ -321,6 +321,151 @@ export function parseDbDate(value) {
321
321
  return new Date(`${value.replace(' ', 'T')}Z`);
322
322
  }
323
323
  // ---------------------------------------------------------------------------
324
+ // Driver type parsers for the zone-less temporal OIDs
325
+ // ---------------------------------------------------------------------------
326
+ /**
327
+ * A Postgres `date` wire value: `YYYY-MM-DD`, optionally with more than four
328
+ * year digits, optionally suffixed ` BC`. Anything else (`infinity`,
329
+ * `-infinity`, and any shape a future server adds) is deliberately NOT matched
330
+ * so it falls through to the driver's own parser untouched.
331
+ */
332
+ const PG_DATE_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})( BC)?$/;
333
+ /**
334
+ * Build the driver parser for Postgres `date` (OID 1082) that reads a
335
+ * zone-less calendar day as **UTC midnight**.
336
+ *
337
+ * The pg default builds the Date from the process's LOCAL zone, so the stored
338
+ * calendar day `2026-07-21` comes back as `2026-07-20T22:00:00Z` in
339
+ * `Europe/Berlin` and `2026-07-20T15:00:00Z` in `Asia/Tokyo`: the wrong
340
+ * calendar day everywhere east of UTC, and the wrong instant everywhere except
341
+ * UTC itself. It is also the exact mirror-image of the WRITE side, which
342
+ * already renders a bound `Date` from its UTC components
343
+ * ({@link toLocalDateTimeLiteral}), so today a read-modify-write cycle on a
344
+ * `date` column east of UTC walks the stored day one day earlier per cycle.
345
+ * This is the missing read half of `utcTimestamps`, matching what
346
+ * {@link parseDbDate} already does for the JSON path and what the OID 1114
347
+ * parser already does for `timestamp`.
348
+ *
349
+ * `fallback` is the parser this one REPLACES, and it must be captured with
350
+ * `pg.types.getTypeParser(1082, 'text')` BEFORE registration (reading it after
351
+ * would hand back this function and recurse forever). It keeps `infinity` /
352
+ * `-infinity` on the driver's `Infinity` / `-Infinity`.
353
+ *
354
+ * `setUTCFullYear` rather than the `Date` constructor, so a two-or-three-digit
355
+ * year is not silently mapped into the 1900s, and ` BC` maps to the
356
+ * astronomical year (`0044 BC` → -43) the way the driver's own parser does.
357
+ */
358
+ export function createUtcDateParser(fallback) {
359
+ return (text) => {
360
+ const m = PG_DATE_TEXT_RE.exec(text);
361
+ if (!m)
362
+ return fallback(text);
363
+ const year = m[4] ? -(Number(m[1]) - 1) : Number(m[1]);
364
+ const date = new Date(0);
365
+ date.setUTCFullYear(year, Number(m[2]) - 1, Number(m[3]));
366
+ date.setUTCHours(0, 0, 0, 0);
367
+ return date;
368
+ };
369
+ }
370
+ /**
371
+ * A Postgres `timestamp` (without time zone) wire value:
372
+ * `YYYY-MM-DD HH:MM:SS`, optionally with more than four year digits, optional
373
+ * fractional seconds, optional ` BC`. As with {@link PG_DATE_TEXT_RE},
374
+ * `infinity` / `-infinity` and any shape a future server adds deliberately do
375
+ * NOT match, so they fall through to the driver's own parser untouched.
376
+ */
377
+ const PG_TIMESTAMP_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?( BC)?$/;
378
+ /**
379
+ * Build the driver parser for Postgres `timestamp` (OID 1114) that reads an
380
+ * offset-less date-time as UTC. Also lifted to the `_timestamp` array OID
381
+ * (1115), so the scalar and the array can never settle on different
382
+ * interpretations.
383
+ *
384
+ * `fallback` is the parser this one REPLACES and must be captured with
385
+ * `pg.types.getTypeParser(1114, 'text')` BEFORE registration (see
386
+ * {@link createUtcDateParser}). It is what keeps `infinity` / `-infinity` on
387
+ * the driver's `Infinity` / `-Infinity`: the earlier
388
+ * `new Date(text.replace(' ', 'T') + 'Z')` form turned `'infinity'` into
389
+ * `'infinityZ'` and so into an `Invalid Date` that flowed on silently.
390
+ *
391
+ * Component assembly rather than `Date` string parsing, for the same reason as
392
+ * the `date` parser: a year outside four digits and a ` BC` suffix are not
393
+ * parseable as ISO-8601 and would otherwise also become `Invalid Date`.
394
+ * Fractional seconds are truncated to milliseconds, which is what
395
+ * `Date`-string parsing did too.
396
+ */
397
+ export function createUtcTimestampParser(fallback) {
398
+ return (text) => {
399
+ const m = PG_TIMESTAMP_TEXT_RE.exec(text);
400
+ if (!m)
401
+ return fallback(text);
402
+ const year = m[8] ? -(Number(m[1]) - 1) : Number(m[1]);
403
+ const ms = m[7] ? Number(m[7].slice(0, 3).padEnd(3, '0')) : 0;
404
+ const date = new Date(0);
405
+ date.setUTCFullYear(year, Number(m[2]) - 1, Number(m[3]));
406
+ date.setUTCHours(Number(m[4]), Number(m[5]), Number(m[6]), ms);
407
+ return date;
408
+ };
409
+ }
410
+ /**
411
+ * The offset-less-timestamp-as-UTC reading, with no fallback: `text` must be a
412
+ * plain `YYYY-MM-DD HH:MM:SS[.ffffff]`. Used where the input shape is already
413
+ * known (tests, JSON-wire coercion); the DRIVER parser is
414
+ * {@link createUtcTimestampParser}, which delegates everything else.
415
+ */
416
+ export function parseUtcTimestampText(text) {
417
+ return new Date(`${text.replace(' ', 'T')}Z`);
418
+ }
419
+ /**
420
+ * Lift an element parser to the matching Postgres array OID.
421
+ *
422
+ * Array OIDs do NOT inherit their element type's parser: registering a parser
423
+ * for `date` (1082) leaves `date[]` (1182) on the driver's default, so the same
424
+ * value read from a scalar column and from an array column would disagree by
425
+ * the process offset. Every scalar temporal parser Turbine registers is
426
+ * therefore registered in its array form too.
427
+ *
428
+ * `pg.types.arrayParser` is a public member of the `pg` module (it is what the
429
+ * driver's own `_text` / `_date` parsers are built from), so this adds no
430
+ * dependency. NULL elements stay `null` and are never handed to `element`.
431
+ */
432
+ export function createPgArrayParser(element) {
433
+ const arrayParser = pg.types.arrayParser;
434
+ return (text) => arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse();
435
+ }
436
+ /**
437
+ * Register the UTC readings of the four zone-less temporal OIDs on the pg
438
+ * module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
439
+ *
440
+ * ONE place, because `pg.types.setTypeParser` is process-global and the pairing
441
+ * matters: registering a scalar without its array form, or a `date` without the
442
+ * `timestamp` beside it, produces two columns of the same row disagreeing about
443
+ * what the same wire text means. Both callers are processes Turbine owns the
444
+ * pg module in: `TurbineClient` on a pool it created (never on an external
445
+ * pool, whose parser configuration belongs to the caller), and `turbine studio`,
446
+ * which builds a raw pool of its own and must render what the application sees.
447
+ *
448
+ * Each fallback is read BEFORE its parser is installed, so an unrecognised wire
449
+ * value (`infinity`, and whatever a future server adds) still reaches the
450
+ * driver's own parser.
451
+ */
452
+ export function registerUtcTemporalParsers() {
453
+ // pg-types declares get/setTypeParser over its own OID enum, which lists the
454
+ // scalar types only. The array OIDs are just as real, so both calls are
455
+ // retyped over a plain number rather than the incomplete enum.
456
+ const getParser = pg.types.getTypeParser;
457
+ const setParser = pg.types.setTypeParser;
458
+ const parseDate = createUtcDateParser(getParser(1082, 'text'));
459
+ const parseTimestamp = createUtcTimestampParser(getParser(1114, 'text'));
460
+ setParser(1114, parseTimestamp);
461
+ setParser(1082, parseDate);
462
+ // Array OIDs do not inherit their element parser, so `date[]` / `timestamp[]`
463
+ // would otherwise keep returning local-zone Dates while the scalar columns
464
+ // beside them returned UTC ones.
465
+ setParser(1182, createPgArrayParser(parseDate));
466
+ setParser(1115, createPgArrayParser(parseTimestamp));
467
+ }
468
+ // ---------------------------------------------------------------------------
324
469
  // JSON-wire value coercion (relationLoadStrategy: 'join')
325
470
  // ---------------------------------------------------------------------------
326
471
  /**
@@ -343,7 +488,7 @@ export function parseDbDate(value) {
343
488
  * numeric '1000.50' (string) 1000.5 (number, LOSSY)
344
489
  * int8 '9007199254740993' 9007199254740992 (LOSSY)
345
490
  * bytea Buffer '\xdeadbeef' (string)
346
- * date Date (local midnight) Date (UTC midnight, off by tz)
491
+ * date Date (UTC midnight) Date (UTC midnight, by coincidence)
347
492
  * interval { days, hours, … } '1 day 02:03:04' (string)
348
493
  * point { x, y } '(1,2)' (string)
349
494
  * circle { x, y, radius } '<(1,2),3>' (string)
@@ -82,4 +82,10 @@ export declare const WARN_NS: {
82
82
  * the offenders, not one per column. The namespace name is historical.
83
83
  */
84
84
  readonly untypedDateColumn: "untypedDateColumn";
85
+ /**
86
+ * `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
87
+ * runs no connection setup, so the option is a no-op (client.ts constructor).
88
+ * Keyed on the requested mode.
89
+ */
90
+ readonly planCacheModeIgnored: "planCacheModeIgnored";
85
91
  };
@@ -117,4 +117,10 @@ export const WARN_NS = {
117
117
  * the offenders, not one per column. The namespace name is historical.
118
118
  */
119
119
  untypedDateColumn: 'untypedDateColumn',
120
+ /**
121
+ * `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
122
+ * runs no connection setup, so the option is a no-op (client.ts constructor).
123
+ * Keyed on the requested mode.
124
+ */
125
+ planCacheModeIgnored: 'planCacheModeIgnored',
120
126
  };
package/dist/sqlite.js CHANGED
@@ -377,6 +377,7 @@ export const sqliteDialect = {
377
377
  supportsAdvisoryLock: false,
378
378
  // No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
379
379
  supportsLateralJoin: false,
380
+ supportsPlanCacheMode: false,
380
381
  // SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
381
382
  // id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
382
383
  explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",