@prisma/orm-mongo 8.0.0-rc.5-dev.2 → 8.0.0-rc.5-dev.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/orm-mongo",
3
- "version": "8.0.0-rc.5-dev.2",
3
+ "version": "8.0.0-rc.5-dev.4",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -10,16 +10,16 @@
10
10
  "skills"
11
11
  ],
12
12
  "dependencies": {
13
- "@prisma/orm-family-mongo": "8.0.0-rc.5-dev.2",
14
- "@prisma/orm-framework": "8.0.0-rc.5-dev.2",
15
- "@prisma/orm-target-mongo": "8.0.0-rc.5-dev.2",
16
- "@prisma/orm-toolchain": "8.0.0-rc.5-dev.2",
13
+ "@prisma/orm-family-mongo": "8.0.0-rc.5-dev.4",
14
+ "@prisma/orm-framework": "8.0.0-rc.5-dev.4",
15
+ "@prisma/orm-target-mongo": "8.0.0-rc.5-dev.4",
16
+ "@prisma/orm-toolchain": "8.0.0-rc.5-dev.4",
17
17
  "pathe": "^2.0.3"
18
18
  },
19
19
  "devDependencies": {
20
- "@internal/mongo": "8.0.0-rc.5-dev.2",
21
- "@repo/tsconfig": "8.0.0-rc.5-dev.2",
22
- "@repo/tsdown": "8.0.0-rc.5-dev.2",
20
+ "@internal/mongo": "8.0.0-rc.5-dev.4",
21
+ "@repo/tsconfig": "8.0.0-rc.5-dev.4",
22
+ "@repo/tsdown": "8.0.0-rc.5-dev.4",
23
23
  "tsdown": "0.22.14",
24
24
  "typescript": "5.9.3"
25
25
  },
@@ -17,7 +17,7 @@ description: >-
17
17
  (schema.prisma + @prisma/client projects).
18
18
  metadata:
19
19
  library: '@prisma/orm-mongo'
20
- library_version: '8.0.0-rc.5-dev.2'
20
+ library_version: '8.0.0-rc.5-dev.4'
21
21
  ---
22
22
 
23
23
  # Prisma Next (Prisma 8)
@@ -0,0 +1,156 @@
1
+ ---
2
+ from: "8.0.0-rc.5"
3
+ to: "8.0.0-rc.6"
4
+ changes:
5
+ - id: postgres-temporal-representations
6
+ summary: |
7
+ PostgreSQL temporal columns no longer read as JavaScript `Date`. Each of `date`,
8
+ `timestamp(p)`, `timestamptz(p)` and `time(p)` now offers two explicit representations,
9
+ and five codecs were removed with no compatibility aliases:
10
+
11
+ | Retired | Replace with (Temporal) | Replace with (text) |
12
+ | --- | --- | --- |
13
+ | `pg/date@1` | `Date` → `pg/date-temporal@1` (`Temporal.PlainDate`) | `DateString` |
14
+ | `pg/timestamp@1` | `Timestamp(p)` → `pg/timestamp-temporal@1` (`Temporal.PlainDateTime`) | `TimestampString(p)` |
15
+ | `pg/timestamptz@1` | `Timestamptz(p)` → `pg/timestamptz-temporal@1` (`Temporal.Instant`) | `TimestamptzString(p)` |
16
+ | `pg/time@1` | `Time(p)` → `pg/time-temporal@1` (`Temporal.PlainTime`) | `TimeString(p)` |
17
+ | `sql/timestamp@1` (`field.timestamp()`) | `Timestamptz(p)` or `field.temporal.timestamptz(p)` | `TimestamptzString(p)` |
18
+
19
+ 1. **Decide a representation per column.** The bare PSL spellings (`Date`, `Timestamp`,
20
+ `Timestamptz`, `Time`) keep working and now select the Temporal-backed codec. If a
21
+ column's values should stay text — because your code treats them as strings, or
22
+ because they include values Temporal cannot denote (`infinity`, non-ISO `DateStyle`
23
+ output, years beyond ±271821) — rename the type to its `*String` spelling. Note
24
+ `pg/time@1` already handed back a `string`: a `time` column whose value you treat as
25
+ text needs `TimeString(p)`, not `Time(p)`.
26
+ 2. **Replace `field.timestamp()`.** The generic cross-target helper and its
27
+ `sql/timestamp@1` codec are gone. On PostgreSQL use `field.temporal.timestamptz(p)`
28
+ (or `field.temporal.timestamptzString(p)`), or the bare `Timestamptz(p)` type.
29
+ 3. **Repoint any codec id you wrote by hand.** Ids appear in raw-lane return
30
+ declarations (``db.raw.sql`now()`.returns('pg/timestamptz@1')``), in
31
+ `prepare({ id })`, and in hand-built contracts. A retired id no longer resolves and
32
+ fails contract validation rather than degrading silently.
33
+ 4. **Re-emit every contract.** `prisma contract emit` rewrites `contract.json` and
34
+ `contract.d.ts` together; the generated application types are where the new
35
+ representation becomes visible. Commit the regenerated artifacts. A contract emitted
36
+ before this release references a codec the registry cannot resolve and is rejected at
37
+ runtime.
38
+ 5. **Provide a Temporal implementation if you kept any Temporal-backed column.** Prisma
39
+ bundles no polyfill. Where the runtime has no native `Temporal`, install a global one
40
+ in your entry point before any query runs — `import 'temporal-polyfill/full/global';`.
41
+ Take `full/global`, not `global`: the default build omits non-ISO calendars and its
42
+ published types resolve to `export {}`, so TypeScript will not see the namespace.
43
+ **Every read** of a Temporal-backed column needs it, and so does any insert into a
44
+ table carrying `temporal.updatedAt()`, whose generated value is a `Temporal.Instant`.
45
+ Without it the operation fails with `RUNTIME.TEMPORAL_UNAVAILABLE`, which names the
46
+ codec and recommends the matching `*String` type. A contract whose temporal columns
47
+ are all `*String` needs no Temporal anywhere.
48
+ 6. **Update application code that consumed a `Date`.** `Temporal.Instant.from()` parses
49
+ **only an ISO string carrying an offset** — it throws on a `Date`, on an
50
+ epoch-millisecond number, and on a date-time string with no offset. Convert by source:
51
+
52
+ | You have | Use |
53
+ | --- | --- |
54
+ | a `Date` | `instant = date.toTemporalInstant()`, or `Temporal.Instant.fromEpochMilliseconds(date.getTime())` |
55
+ | epoch milliseconds | `Temporal.Instant.fromEpochMilliseconds(ms)` |
56
+ | an ISO string **with** an offset (`…Z`, `…+02:00`) | `Temporal.Instant.from(text)` |
57
+ | an ISO string **without** an offset | pick the zone it meant: `Temporal.PlainDateTime.from(text).toZonedDateTime('UTC').toInstant()` |
58
+ | "now" | `Temporal.Now.instant()` |
59
+
60
+ Match the column, not just the type name: a `date` column takes a
61
+ `Temporal.PlainDate` (`Temporal.PlainDate.from('2024-01-01')`), a `timestamp` column a
62
+ `Temporal.PlainDateTime`, a `time` column a `Temporal.PlainTime`. Only `timestamptz`
63
+ takes an `Instant`.
64
+
65
+ `Temporal.Instant.compare(a, b)` replaces `a.getTime() - b.getTime()`, but **both
66
+ operands must already be `Instant`s** — it throws on a `Date`. Values read back from
67
+ the ORM already are; convert anything you brought from elsewhere first.
68
+
69
+ In tests, be careful with **`toMatchObject`**. A Temporal value has no own enumerable
70
+ properties — every accessor lives on the prototype — so a subset matcher finds nothing
71
+ to compare and passes for *any* value of the same type. `toEqual` is not affected
72
+ (Vitest compares these correctly), but `toMatchObject` will silently stop checking the
73
+ timestamp. Where you need a subset match, compare `toString()` or use the type's own
74
+ `equals` / `compare`.
75
+ detection:
76
+ glob: "**/*.{ts,mts,cts,prisma,json}"
77
+ regex:
78
+ - "pg/(date|timestamp|timestamptz|time)@1"
79
+ - "sql/timestamp@1"
80
+ - "field\\.timestamp\\("
81
+ anyMatch: true
82
+ - id: literal-default-needs-the-string-spelling
83
+ summary: |
84
+ A literal `@default(...)` on a Temporal-backed temporal column cannot be emitted today.
85
+ The default value is encoded through the column's codec while the contract is being
86
+ emitted, inside the CLI's own process, and stock Node ships no `Temporal`. So
87
+
88
+ ```prisma
89
+ occurredAt Timestamptz @default("2024-01-01T00:00:00Z")
90
+ ```
91
+
92
+ fails `prisma contract emit` with `CONTRACT.SOURCE_LOAD_FAILED` and "this runtime has no
93
+ global Temporal implementation".
94
+
95
+ Use the string spelling for a column that needs a literal default:
96
+
97
+ ```prisma
98
+ occurredAt TimestamptzString @default("2024-01-01T00:00:00Z")
99
+ ```
100
+
101
+ Function defaults are unaffected — `@default(now())` lowers to a PostgreSQL `now()`
102
+ storage default, never passes through a codec, and works on either representation.
103
+ detection:
104
+ glob: "**/*.prisma"
105
+ regex:
106
+ - "(Date|Timestamp|Timestamptz|Time)(\\([0-9]+\\))?\\s+@default\\(\""
107
+ anyMatch: true
108
+
109
+ ---
110
+
111
+ # 8.0.0-rc.4 → 8.0.0-rc.5 — User upgrade instructions
112
+
113
+ ## `attach-pg-client-error-listener`
114
+
115
+ Walk every file matched by `detection.glob`. For each `pg.Client` or `pg.Pool` your code constructs and uses directly (not one handed to `postgres({ pg: ... })` / `supabase({ pg: ... })` — the runtime covers those since rc.5), attach an `'error'` listener right after construction, before `connect()`:
116
+
117
+ ```ts
118
+ const client = new pg.Client({ connectionString });
119
+ client.on('error', () => {});
120
+ await client.connect();
121
+ ```
122
+
123
+ A no-op listener is enough: connect and query failures still reject their own promises, so nothing real is masked — the listener only stops a dropped idle connection from becoming an uncaught exception. If the handle is long-lived and you have a logging channel, log the error instead of discarding it.
124
+
125
+ Note that a surrounding `try/catch` does **not** cover this case — the `'error'` event is emitted on the client object asynchronously, outside any promise chain the `catch` can see.
126
+
127
+ # 8.0.0-rc.4 → 8.0.0-rc.5 — User upgrade instructions
128
+
129
+ ## `attach-pg-client-error-listener`
130
+
131
+ Walk every file matched by `detection.glob`. For each `pg.Client` or `pg.Pool` your code constructs and uses directly (not one handed to `postgres({ pg: ... })` / `supabase({ pg: ... })` — the runtime covers those since rc.5), attach an `'error'` listener right after construction, before `connect()`:
132
+
133
+ ```ts
134
+ const client = new pg.Client({ connectionString });
135
+ client.on('error', () => {});
136
+ await client.connect();
137
+ ```
138
+
139
+ A no-op listener is enough: connect and query failures still reject their own promises, so nothing real is masked — the listener only stops a dropped idle connection from becoming an uncaught exception. If the handle is long-lived and you have a logging channel, log the error instead of discarding it.
140
+
141
+ Note that a surrounding `try/catch` does **not** cover this case — the `'error'` event is emitted on the client object asynchronously, outside any promise chain the `catch` can see.
142
+
143
+ # PostgreSQL temporal representations
144
+
145
+ Both entries are schema-and-code renames; there is no codemod, because the choice between the
146
+ Temporal and the string representation is per column and only you know which values a column
147
+ holds. Work through the schema first, re-emit, then let the generated `contract.d.ts` types drive
148
+ the application-code changes — the compiler will point at every site whose value type moved.
149
+
150
+ Two behaviours are worth knowing before you choose. A Temporal codec rejects what `Temporal` cannot
151
+ denote — `infinity`, years beyond roughly ±271821, and output rendered under a non-ISO `DateStyle` —
152
+ naming the `*String` type that reads the same column losslessly. And a nested read returns the same
153
+ text a flat read does, because temporal expressions are cast to `text` before PostgreSQL builds the
154
+ JSON, which means both reflect the session `TimeZone`.
155
+
156
+
@@ -0,0 +1,192 @@
1
+ ---
2
+ from: "8.0.0-rc.5"
3
+ to: "8.0.0-rc.6"
4
+ changes:
5
+ - id: postgres-temporal-codec-ids-retired
6
+ summary: |
7
+ Five PostgreSQL temporal codec ids were removed with no compatibility aliases. Each
8
+ native type now has two representation-explicit codecs — one whose application value is a
9
+ `Temporal.*`, one that passes PostgreSQL's own text through unchanged:
10
+
11
+ | Retired | Temporal replacement | Text replacement |
12
+ | --- | --- | --- |
13
+ | `pg/date@1` | `pg/date-temporal@1` (`Temporal.PlainDate`) | `pg/date-string@1` |
14
+ | `pg/timestamp@1` | `pg/timestamp-temporal@1` (`Temporal.PlainDateTime`) | `pg/timestamp-string@1` |
15
+ | `pg/timestamptz@1` | `pg/timestamptz-temporal@1` (`Temporal.Instant`) | `pg/timestamptz-string@1` |
16
+ | `pg/time@1` | `pg/time-temporal@1` (`Temporal.PlainTime`) | `pg/time-string@1` |
17
+ | `sql/timestamp@1` | `pg/timestamptz-temporal@1` | `pg/timestamptz-string@1` |
18
+
19
+ An extension names these ids in more places than a user does. Sweep all of them:
20
+
21
+ 1. **Column descriptors in an extension contract.** A pack that declares its own tables
22
+ (`extensionModel(...)` with `{ codecId, nativeType }` column literals) picks the
23
+ representation on its consumers' behalf. Choose the Temporal id where application code
24
+ reads the column as a value, and the `*String` id where it should stay text — the
25
+ Supabase pack's `auth` tables took the Temporal id for exactly that reason.
26
+ 2. **`descriptor-meta` registrations, control-plane hooks and aggregate matrices.** Any
27
+ table keyed by codec id gains two entries where it had one, or moves its single entry.
28
+ A parity or coverage table that enumerates ids needs the four new `*-string@1` ids as
29
+ well as the four `*-temporal@1` ones.
30
+ 3. **Hand-built contracts and test doubles.** Deserializing a contract literal that names
31
+ a retired id now fails validation rather than resolving to something plausible.
32
+ 4. **Introspection maps.** `date`, `timestamp`, `timestamptz` and `time` map to the bare
33
+ PSL names (`Date`, `Timestamp(p)`, `Timestamptz(p)`, `Time(p)`), which resolve to the
34
+ Temporal codecs. The `*String` names are authoring-only and must claim no
35
+ `targetTypes`, or they compete for introspection ownership.
36
+ 5. **Re-emit any contract your package commits.** `build:contract-space` (or
37
+ `prisma contract emit`) rewrites `contract.json` and `contract.d.ts`; commit both.
38
+ detection:
39
+ glob: "**/*.{ts,mts,cts,json}"
40
+ regex:
41
+ - "pg/(date|timestamp|timestamptz|time)@1"
42
+ - "sql/timestamp@1"
43
+ anyMatch: true
44
+ - id: temporal-codecs-require-a-global-and-refuse-a-date
45
+ summary: |
46
+ A Temporal-backed codec reads the application's global `Temporal` implementation. Prisma
47
+ neither bundles nor imports a polyfill, and the check is lazy: registering a pack,
48
+ validating a contract, resolving a descriptor and constructing a codec instance all
49
+ succeed with no `Temporal` in scope. Only invoking one fails, with
50
+ `RUNTIME.TEMPORAL_UNAVAILABLE`.
51
+
52
+ Two consequences for an extension:
53
+
54
+ 1. **Your test suites need the global.** If your package exercises a Temporal-backed
55
+ column, install a polyfill in a vitest `setupFiles` entry
56
+ (`import 'temporal-polyfill/full/global';`) and add `temporal-polyfill` as a
57
+ devDependency. For TypeScript to see the same global, add a `.d.ts` under your test
58
+ directory containing `/// <reference types="temporal-polyfill/types/global" />` — the
59
+ package's own `temporal-polyfill/global` types resolve to `export {}` and declare
60
+ nothing.
61
+ 2. **Encode is nominally typed now.** These codecs check `Symbol.toStringTag` and refuse
62
+ anything that is not their own Temporal type, including a `Date`, with
63
+ `RUNTIME.ENCODE_FAILED` naming the codec. If your pack contributes a mutation-default
64
+ generator or any other value that lands in a Temporal-backed column, it must produce
65
+ the matching `Temporal.*` value — a `Date` no longer slips through to be serialized as
66
+ `Date.prototype.toString()`.
67
+ detection:
68
+ glob: "**/*.{ts,mts,cts}"
69
+ regex:
70
+ - "pg/(date|timestamp|timestamptz|time)-temporal@1"
71
+ anyMatch: true
72
+
73
+ ---
74
+
75
+ # 8.0.0-rc.4 → 8.0.0-rc.5 — Extension author upgrade instructions
76
+
77
+ ## `wrap-pg-constructions-with-suppress-idle-connection-errors`
78
+
79
+ Walk every file matched by `detection.glob`. For each pg `Pool` or `Client` the extension constructs, wrap the construction:
80
+
81
+ ```ts
82
+ import { suppressIdleConnectionErrors } from '@internal/driver-postgres/runtime';
83
+
84
+ const pool = suppressIdleConnectionErrors(
85
+ new Pool({ connectionString: options.url }),
86
+ );
87
+ ```
88
+
89
+ This is the same translation applied to the in-repo `@internal/postgres` and `@internal/extension-supabase` runtimes in this transition. The helper only attaches a no-op `'error'` listener (connect/query failures still reject their own promises), so behavior is otherwise unchanged; without it, a dropped idle connection crashes the process that hosts the extension.
90
+
91
+ If your extension's test suite fakes the `pg` module, the fakes need an `on` method (`on = vi.fn().mockReturnThis()` on a class fake, or `on: vi.fn()` on an object literal) — the runtime now calls `.on('error', ...)` on every pool, client, and checked-out pool client.
92
+ ## `distinct-on-requires-postgres-capability`
93
+
94
+ `Collection#distinctOn(...)` used to compile and run on any target, but only Postgres ever
95
+ rendered its `DISTINCT ON` clause — a call on any other target (SQLite) compiled clean and
96
+ silently returned undeduped rows at runtime. The method now carries the same capability gate the
97
+ sql-builder lane already enforces: its parameter type narrows to `never` unless the contract
98
+ declares `postgres.distinctOn`, so the same call is a compile error on a contract that lacks it,
99
+ and a runtime error carrying `ORM.CAPABILITY_MISSING` if reached dynamically (e.g. through a
100
+ hand-built `CollectionState`).
101
+
102
+ Find every `.distinctOn(...)` call your code makes on a `Collection` and check whether the
103
+ contract it runs against declares `postgres.distinctOn`. If it does, nothing changes — the call
104
+ already worked correctly and keeps compiling. If it does not, the call was already producing the
105
+ wrong result set; either move the collection onto a Postgres-capable contract, or remove the
106
+ `.distinctOn(...)` call and accept the undeduped rows it was silently returning before.
107
+
108
+ `Collection#distinct(...)` is unaffected — it lowers to a portable `ROW_NUMBER` dedup and needs
109
+ no capability, on any target.
110
+
111
+ ## `groupby-pre-group-pagination-now-scopes-rows`
112
+
113
+ Any `.take(...)`, `.skip(...)`, `.cursor(...)`, `.distinct(...)`, `.distinctOn(...)`, or
114
+ `.orderBy(...)` your extension calls *before* `.groupBy(...)` on a `Collection` used to be
115
+ silently dropped once `.groupBy(...)` joined the chain — the aggregate reduced over every
116
+ matching row, ignoring the pagination clause entirely. It now scopes the rows that get grouped,
117
+ the same way root `.aggregate()` scopes its rows (see the sibling entry for that fix, already
118
+ shipped in `8.0.0-rc.4` → `8.0.0-rc.5`'s predecessor window).
119
+
120
+ There is no detection regex for this one worth writing: the call sites that need re-checking
121
+ look identical, in source, to the call sites that already worked correctly (a chain built with
122
+ this scoping in mind, versus one that assumed the pagination clause was a no-op). Grep for
123
+ `.groupBy(` and read every match with a pre-group pagination clause; if the test asserting its
124
+ result seeds fewer distinct groups than pagination scope allows, or asserts totals computed over
125
+ every row rather than the paginated window, the expected values need updating to match the now-
126
+ correct behavior.
127
+
128
+ ## `groupby-post-group-pagination-requires-order-by`
129
+
130
+ `GroupedCollection` (what `.groupBy(...)` returns) gained `take()`, `skip()`, and `orderBy()`,
131
+ which page the *grouped* rows when written after `.groupBy(...)` — previously `.groupBy(...)` had
132
+ no chain of its own past `.having(...)`. Calling post-group `take()` or `skip()` without a prior
133
+ post-group `orderBy()` is a compile error: the parameter type narrows to `never`, because a
134
+ database may return groups in any order and "the first n groups" has no defined meaning without
135
+ one.
136
+
137
+ If your extension's own code (or its test suite) calls `.groupBy(...).take(...)` or
138
+ `.groupBy(...).skip(...)` with no `.orderBy(...)` between them, it will fail to compile after this
139
+ upgrade. There is no default ordering to insert automatically — add an `.orderBy(...)` naming one
140
+ of the fields you passed to `groupBy(...)` (ascending or descending is your call; whichever
141
+ matches what "the first n groups" should mean for that query) before the `take()` / `skip()` call.
142
+
143
+ # 8.0.0-rc.4 → 8.0.0-rc.5 — Extension author upgrade instructions
144
+
145
+ ## `wrap-pg-constructions-with-suppress-idle-connection-errors`
146
+
147
+ Walk every file matched by `detection.glob`. For each pg `Pool` or `Client` the extension constructs, wrap the construction:
148
+
149
+ ```ts
150
+ import { suppressIdleConnectionErrors } from '@internal/driver-postgres/runtime';
151
+
152
+ const pool = suppressIdleConnectionErrors(
153
+ new Pool({ connectionString: options.url }),
154
+ );
155
+ ```
156
+
157
+ This is the same translation applied to the in-repo `@internal/postgres` and `@internal/extension-supabase` runtimes in this transition. The helper only attaches a no-op `'error'` listener (connect/query failures still reject their own promises), so behavior is otherwise unchanged; without it, a dropped idle connection crashes the process that hosts the extension.
158
+
159
+ If your extension's test suite fakes the `pg` module, the fakes need an `on` method (`on = vi.fn().mockReturnThis()` on a class fake, or `on: vi.fn()` on an object literal) — the runtime now calls `.on('error', ...)` on every pool, client, and checked-out pool client.
160
+ ## `distinct-on-requires-postgres-capability`
161
+
162
+ `Collection#distinctOn(...)` used to compile and run on any target, but only Postgres ever
163
+ rendered its `DISTINCT ON` clause — a call on any other target (SQLite) compiled clean and
164
+ silently returned undeduped rows at runtime. The method now carries the same capability gate the
165
+ sql-builder lane already enforces: its parameter type narrows to `never` unless the contract
166
+ declares `postgres.distinctOn`, so the same call is a compile error on a contract that lacks it,
167
+ and a runtime error carrying `ORM.CAPABILITY_MISSING` if reached dynamically (e.g. through a
168
+ hand-built `CollectionState`).
169
+
170
+ Find every `.distinctOn(...)` call your code makes on a `Collection` and check whether the
171
+ contract it runs against declares `postgres.distinctOn`. If it does, nothing changes — the call
172
+ already worked correctly and keeps compiling. If it does not, the call was already producing the
173
+ wrong result set; either move the collection onto a Postgres-capable contract, or remove the
174
+ `.distinctOn(...)` call and accept the undeduped rows it was silently returning before.
175
+
176
+ `Collection#distinct(...)` is unaffected — it lowers to a portable `ROW_NUMBER` dedup and needs
177
+ no capability, on any target.
178
+
179
+ # PostgreSQL temporal representations, for extension authors
180
+
181
+ There is no codemod: the retired ids map to *two* replacements each, and which one an extension
182
+ should name is a judgement about what its consumers do with the column. Sweep by id, decide per
183
+ site, then re-emit any committed contract artifact.
184
+
185
+ Four behaviours bear on an extension's own codecs. Writes serialize at full precision and let
186
+ PostgreSQL round to the column's declared precision, carries included. A Temporal codec rejects
187
+ `infinity`, years beyond roughly ±271821, and non-ISO `DateStyle` output, naming the `*String` type
188
+ that reads them losslessly. The driver hands temporal OIDs through as server text rather than
189
+ building a `Date`. And temporal expressions are cast to `text` before PostgreSQL builds JSON, so a
190
+ nested read returns the same text a flat one does.
191
+
192
+