Skip to content

fix(target-postgres): brand Timestamp(tz)<P> on Date#429

Open
saevarb wants to merge 1 commit intomainfrom
fix/timestamp-codec-brand-base
Open

fix(target-postgres): brand Timestamp(tz)<P> on Date#429
saevarb wants to merge 1 commit intomainfrom
fix/timestamp-codec-brand-base

Conversation

@saevarb
Copy link
Copy Markdown
Contributor

@saevarb saevarb commented May 6, 2026

closes TML-2391

Intent

Resolve a self-contradiction inside @prisma-next/target-postgres: the package's pg/timestamp@1 / pg/timestamptz@1 codecs declare Codec<…, Date, Date, …> and decode to Date, but the package's emitter-facing aliases Timestamp<P> / Timestamptz<P> were branded on string. For columns declared with a precision (timestamp(3) etc.), the contract emitter wrote those branded-string aliases into contract.d.ts, so consumers calling .toISOString() or instanceof Date on a projected column tripped a TS error even though the runtime value was a Date. The fix re-bases the two branded aliases on Date so the type matches the codec's declared input/output.

Before / After (packages/3-targets/3-targets/postgres/src/exports/codec-types.ts)

// BEFORE
export type Timestamp<P extends number | undefined = undefined> = BrandedString<{
  __timestampPrecision: P;
}>;
export type Timestamptz<P extends number | undefined = undefined> = BrandedString<{
  __timestamptzPrecision: P;
}>;
// AFTER
type BrandedDate<Shape extends Record<string, unknown>> = Branded<Date, Shape>;

// `Timestamp` / `Timestamptz` brand `Date` — the `pg/timestamp@1` and
// `pg/timestamptz@1` codecs decode to `Date`. Branding `string` here would
// contradict the codec's declared input/output and force consumers to cast
// before calling Date methods on a projected column.
export type Timestamp<P extends number | undefined = undefined> = BrandedDate<{
  __timestampPrecision: P;
}>;
export type Timestamptz<P extends number | undefined = undefined> = BrandedDate<{
  __timestamptzPrecision: P;
}>;

The other parameterized aliases (Char<N>, Varchar<N>, Numeric<P,S>, Bit<N>, VarBit<N>, Time<P>, Timetz<P>, Interval<P>) stay branded on string — their codecs decode to string, so the brand base was already correct.

Change map

The story

  1. The runtime codec was already telling the truth. pgTimestampCodec and pgTimestamptzCodec in packages/3-targets/3-targets/postgres/src/core/codecs.ts are explicitly parametrized as Codec<…, Date, Date, …> with decode: (wire) => wire — i.e. the package's own statement is "input is Date, output is Date."
  2. The emitter-facing aliases disagreed. In exports/codec-types.ts, Timestamp<P> and Timestamptz<P> were branded on string. For a column with a precision specifier, the emitter calls codec.renderOutputType({ precision }), gets the literal name 'Timestamp<3>', and writes that alias into contract.d.ts — so the consumer's projected field was typed string & { __timestampPrecision: 3 } while the runtime value was a Date.
  3. The minimal fix is to re-base just those two aliases on Date. No emitter logic, no codec, no contract serialization, no descriptor metadata changes — the contradiction was localized to two type aliases in one file.
  4. A new .test-d.ts regression locks the relationship in place, both the positive direction (Timestamp(tz)<P> extends Date) and the negative one (not extends string), plus a check that the alias matches CodecInput<…> of the codec, so future drift between the codec generic and the emitter alias gets caught at typecheck time.

Behavior changes & evidence

  • Branded aliases for parameterized timestamps now match the codec's declared output type. Reading a timestamp(P) / timestamptz(P) column through the runtime returns a value that TypeScript sees as Date & { brand }. .toISOString(), instanceof Date, and the rest of the Date surface typecheck without casts. The previous as unknown as Date workaround consumers used (e.g. pdp-control-plane's management-api handlers) is no longer required.

  • Other parameterized aliases unchanged. Char<N>, Varchar<N>, Numeric<P,S>, Bit<N>, VarBit<N>, Time<P>, Timetz<P>, Interval<P> correctly stay branded on string — their codecs decode to string. A regression test pins this so a future "consistency" pass doesn't accidentally Date-brand them too.

Compatibility / migration / risk

  • No runtime behavior change. The codec is a JS-level identity; the diff is purely in the TypeScript type that the emitter writes for parameterized timestamp columns.
  • No breaking call-site fallout. Verified across the workspace: the only existing real usage of the parameterized form is test/e2e/framework/test/fixtures/generated/contract.d.ts:86 (readonly createdAt: Timestamptz<3> | null;), and no test exercises Date methods on it. All other generated contract.d.ts files import Timestamp / Timestamptz but use the unparameterized CodecTypes['pg/timestamptz@1']['output'] (= Date) accessor at field sites — those imports are dead. Zero references to the brand keys __timestampPrecision / __timestamptzPrecision exist outside the type definition itself.
  • Workspace gates. pnpm typecheck (workspace-wide, packages), pnpm test:packages, pnpm test:integration, pnpm test:e2e, and the target-postgres pnpm lint all pass on the branch.
  • Sibling codec covered transitively. sql/timestamp@1 (packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts) has the same Codec<…, Date, Date, …> shape and renders the same 'Timestamp<P>' name. Under the postgres adapter it imports the same alias from @prisma-next/target-postgres/codec-types, so it inherits the fix without any change.
  • No fixture regen needed. Generated contracts in the repo don't bake the brand structure — they just name the alias.

Follow-ups / open questions

  • The Mongo target was investigated as a candidate for the same fix; it doesn't have the contradiction (mongo/date@1 is declared directly as { input: Date; output: Date } with no parameterized branded alias intermediary). The FL-03 entry in projects/mongo-example-apps/framework-limitations.md references a non-existent mongo/dateTime@1 codec and looks stale — separate cleanup, not part of this PR.
  • A SQL dialect that ever ships its own Timestamp brand from a separate codec-types module would need the analogous fix in that module. None exist today.

Non-goals / intentionally out of scope

  • Changing the codec's runtime behavior (encode/decode functions). The runtime was already correct; this PR only makes the types tell the same story.
  • Removing or restructuring renderOutputType for these codecs (Option B in the ticket). Option A is the smaller, more conservative fix; Option B was considered and rejected for losing the precision-in-hover affordance.
  • Renaming or unifying the brand key shape across other parameterized aliases.

Summary by CodeRabbit

Release Notes

  • New Features

    • Updated Timestamp and Timestamptz types to properly represent Date objects, providing more accurate type definitions for timestamp handling.
  • Tests

    • Added type-level tests to validate timestamp type definitions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@saevarb has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 4 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: d8270dcb-b9aa-42b9-bb93-c2ce2d3c53d9

📥 Commits

Reviewing files that changed from the base of the PR and between 6780555 and f33c9dd.

📒 Files selected for processing (2)
  • packages/3-targets/3-targets/postgres/src/exports/codec-types.ts
  • packages/3-targets/3-targets/postgres/test/codec-types.test-d.ts
📝 Walkthrough

Walkthrough

The PR updates PostgreSQL codec types to brand Timestamp and Timestamptz with Date instead of string by introducing a new BrandedDate type alias, along with type-level tests to validate the Date-shaped brands and their CodecInput mappings.

Changes

Type Definitions and Validation

Layer / File(s) Summary
Type Alias Definition
packages/3-targets/3-targets/postgres/src/exports/codec-types.ts
Introduces BrandedDate<Shape extends Record<string, unknown>> and migrates Timestamp and Timestamptz to use BrandedDate instead of BrandedString. Adds comments explaining Date-branding rationale.
Type-Level Tests
packages/3-targets/3-targets/postgres/test/codec-types.test-d.ts
Validates that Timestamp and Timestamptz brands are Date-shaped, their CodecInput-derived inputs map to Date, and other parameterized aliases remain string-shaped.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


🐰 Timestamps now wear their Date with pride,
No strings attached, just types with Date inside,
Branded and bold in the codec's embrace,
Type tests now verify each temporal place. 🕐✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'fix(target-postgres): brand Timestamp(tz)

on Date' accurately summarizes the main change: migrating Timestamp and Timestamptz types from string branding to Date branding, addressing the core type inconsistency.

Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/timestamp-codec-brand-base

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 6, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@429

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@429

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@429

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@429

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-arktype-json@429

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@429

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@429

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@429

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@429

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@429

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@429

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@429

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@429

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@429

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@429

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@429

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@429

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@429

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@429

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@429

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@429

@prisma-next/ts-render

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ts-render@429

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@429

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@429

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@429

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@429

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@429

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@429

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@429

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@429

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@429

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@429

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@429

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@429

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@429

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@429

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@429

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@429

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@429

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@429

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-builder@429

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@429

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@429

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@429

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@429

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@429

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@429

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@429

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@429

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@429

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@429

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@429

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@429

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@429

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@429

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@429

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@429

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@429

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@429

commit: f33c9dd

The pg/timestamp@1 and pg/timestamptz@1 codecs are declared
Codec<…, Date, Date, …> and pass the wire value through unchanged, but
the emitter-facing Timestamp<P> / Timestamptz<P> aliases were branded
on string. For columns with a precision specifier the emitter wrote
those branded-string aliases into contract.d.ts, so consumers calling
Date methods on a projected timestamp(P) / timestamptz(P) field tripped
a TS error even though the runtime value was a Date.

Re-base both aliases on a new BrandedDate helper so the type matches
the codec declaration. Other parameterized aliases (Char/Varchar/Numeric
/Bit/VarBit/Time/Timetz/Interval) correctly stay branded on string —
their codecs decode to string.

Add a .test-d.ts that pins both directions plus the alias-vs-CodecInput
agreement, so future drift between the codec generic and the emitter
alias gets caught at typecheck time.

Closes TML-2391
@saevarb saevarb force-pushed the fix/timestamp-codec-brand-base branch from 6780555 to f33c9dd Compare May 6, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant