Skip to content

fix(orm): format Date as HH:MM:SS for @db.Time / @db.Timetz columns (#2633)#2634

Open
erwan-joly wants to merge 2 commits intozenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
Open

fix(orm): format Date as HH:MM:SS for @db.Time / @db.Timetz columns (#2633)#2634
erwan-joly wants to merge 2 commits intozenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization

Conversation

@erwan-joly
Copy link
Copy Markdown
Contributor

@erwan-joly erwan-joly commented Apr 30, 2026

Fixes #2633.

Background

PostgresCrudDialect.transformInput converted every DateTime input to Date.prototype.toISOString(), which Postgres TIME / TIMETZ columns reject with:

error: invalid input syntax for type time: "2026-04-29T21:00:00.000Z"
code: 22007

This is the write-side counterpart to #2590 (which fixed @db.Time reads silently returning raw strings).

Approach

The dialect now inspects the field's @db.* attribute and routes time-of-day values to a formatTimeOfDay helper that emits PG-acceptable HH:MM:SS.fff (or HH:MM:SS.fff+ZZ:ZZ for TIMETZ). All other DateTime columns (TIMESTAMP / TIMESTAMPTZ / DATE) keep the existing ISO behaviour — Postgres accepts ISO input for those types natively.

To reach the field's attributes from the dialect, BaseCrudDialect.transformInput gains an optional 4th parameter, fieldDef?: FieldDef. The default behaviour is unchanged for callers that don't pass it.

Files changed

  • packages/orm/src/client/crud/dialects/base-dialect.ts — adds optional fieldDef param to transformInput.
  • packages/orm/src/client/crud/dialects/postgresql.ts — adds formatTimeOfDay helper; transformInput reads @db.Time / @db.Timetz and routes through it.
  • packages/orm/src/client/crud/operations/base.ts — threads fieldDef through every transformInput write call site (create, createMany, update, upsert, automatic updatedAt).
  • tests/regression/test/issue-2633.test.ts — regression covering single create and nested createMany against both @db.Time and @db.Timetz, plus a round-trip read.

Repro

model TradingHour {
  id    Int      @id @default(autoincrement())
  open  DateTime @db.Time(6)
  close DateTime @db.Time(6)
}
// 3.6.4 — throws PG 22007
// this PR — works
await db.tradingHour.create({
  data: {
    open:  new Date('1970-01-01T09:00:00.000Z'),
    close: new Date('1970-01-01T16:30:00.000Z'),
  },
});

Notes

  • Filter call sites that compare against TIME columns benefit from the same coercion via the array-filter path. The other base-dialect filter call sites (top-level scalar comparisons, JSON null checks, etc.) keep the simpler 3-arg form because they don't have field-level context handy. If a user files a follow-up about WHERE open = ? against TIME, that's the natural extension point.
  • Pairs naturally with fix(orm): coerce ISO strings on DateTime input, with strictDateInput opt-in (#2631) #2632 (input validator coercion) — together they restore parity with Prisma's longstanding @db.Time ergonomics.

Summary by CodeRabbit

  • New Features

    • Dialect-aware input handling so native column metadata is respected during input transformation.
  • Bug Fixes

    • Improved DateTime serialization for PostgreSQL time-type columns to preserve time-only values and format them correctly instead of forcing full ISO datetime.
  • Tests

    • Added regression test verifying Date inputs round-trip correctly for native PostgreSQL time/timetz columns.

…enstackhq#2633)

PostgresCrudDialect.transformInput converted every DateTime input to
`Date.prototype.toISOString()`, which Postgres TIME / TIMETZ columns
reject with `22007: invalid input syntax for type time:
"2026-04-29T21:00:00.000Z"`. The dialect now inspects the field's
`@db.*` attribute and formats time-of-day values as `HH:MM:SS.fff`
(or `HH:MM:SS.fff+ZZ:ZZ` for TIMETZ); all other DateTime columns
(including TIMESTAMP / TIMESTAMPTZ / DATE) keep the existing ISO
behaviour because Postgres accepts ISO input for those types.

The fix is the write-side counterpart to zenstackhq#2590, which fixed the read
side of `@db.Time` (raw strings being silently returned instead of
Date objects).

Implementation:
  - `BaseCrudDialect.transformInput` gains an optional 4th parameter,
    `fieldDef?: FieldDef`, so dialects can read `@db.*` native-type
    attributes. The default behaviour is unchanged for callers that
    don't pass it (filter helpers in base-dialect.ts continue to work
    on simple type info).
  - `PostgresCrudDialect.transformInput` reads the field's `@db.*`
    attribute and routes `@db.Time` / `@db.Timetz` to a new
    `formatTimeOfDay` helper that emits the PG-acceptable format.
  - All write call sites in `base.ts` (create, createMany, update,
    upsert, automatic `updatedAt`) thread the field def through.
  - Filter call sites that compare against TIME columns also benefit
    from the same coercion via the array-filter path.

Tested against `@db.Time` and `@db.Timetz` with single create and
nested createMany.

Fixes zenstackhq#2633
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5782e1cc-151a-4715-b3c3-e81d583b4b61

📥 Commits

Reviewing files that changed from the base of the PR and between 437018e and fd95c47.

📒 Files selected for processing (1)
  • tests/regression/test/issue-2633.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/regression/test/issue-2633.test.ts

📝 Walkthrough

Walkthrough

The dialect layer's transformInput now accepts an optional fieldDef so dialects can use field metadata when transforming inputs. The Postgres dialect uses this to format DateTime values for @db.Time/@db.Timetz columns as time strings. CRUD operations pass the resolved FieldDef to transformInput.

Changes

Cohort / File(s) Summary
Base Dialect Infrastructure
packages/orm/src/client/crud/dialects/base-dialect.ts
Updated BaseCrudDialect.transformInput signature to add an optional fieldDef?: FieldDef parameter; default behavior unchanged.
PostgreSQL Dialect Implementation
packages/orm/src/client/crud/dialects/postgresql.ts
transformInput now accepts fieldDef and, for DateTime inputs, parses and formats values as HH:MM:SS(.fff)[+TZ] when the field is @db.Time/@db.Timetz; otherwise falls back to ISO serialization; invalid dates left unchanged.
CRUD Operations
packages/orm/src/client/crud/operations/base.ts
All callers updated to pass the resolved FieldDef into transformInput for create/createMany, update, scalar/list updates, and default/generated/updatedAt filling.
Regression Tests
tests/regression/test/issue-2633.test.ts
Added tests verifying PostgreSQL @db.Time(6) and @db.Timetz(6) accept JavaScript Date inputs and round-trip correctly via create/createMany and fetch.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I nudge the clock with fluffy paws and cheer,
I turn the ISO into HH:MM:SS here,
Fields whisper their types, I listen and bind,
Postgres smiles — no parse error to find! 🕐✨

🚥 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 title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check ✅ Passed All code changes align with #2633 objectives: fieldDef parameter added to transformInput signature, PostgreSQL dialect now detects @db.Time/@db.Timetz and formats as HH:MM:SS instead of ISO, and regression test validates the fix.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing #2633: dialect signatures, PostgreSQL time formatting logic, operation layer fieldDef threading, and regression testing—no unrelated modifications present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/regression/test/issue-2633.test.ts (1)

52-57: ⚡ Quick win

Strengthen round-trip assertions with exact time checks.

toBeInstanceOf(Date) alone won’t catch time-shift regressions. Please assert the actual UTC time values too.

Suggested assertion upgrade
             const rows = await client.tradingHour.findMany({ orderBy: { id: 'asc' } });
             expect(rows).toHaveLength(2);
             // The application reads `tw.open` / `tw.close` as Date objects.
             expect(rows[0].open).toBeInstanceOf(Date);
             expect(rows[0].close).toBeInstanceOf(Date);
+            expect(rows[0].open.toISOString()).toBe('1970-01-01T09:00:00.000Z');
+            expect(rows[0].close.toISOString()).toBe('1970-01-01T16:00:00.000Z');
+            expect(rows[1].open.toISOString()).toBe('1970-01-01T10:30:00.000Z');
+            expect(rows[1].close.toISOString()).toBe('1970-01-01T17:30:00.000Z');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/regression/test/issue-2633.test.ts` around lines 52 - 57, The test
currently only checks types using toBeInstanceOf(Date) for rows[0].open and
rows[0].close; strengthen it by asserting exact UTC times to catch time-shift
regressions: after fetching rows with client.tradingHour.findMany, compare
rows[0].open and rows[0].close (and any other relevant rows) against the
expected UTC values (e.g., via toISOString() or constructing new
Date(Date.UTC(...))) so the assertions verify the precise UTC timestamp in
addition to being Date instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/regression/test/issue-2633.test.ts`:
- Around line 52-57: The test currently only checks types using
toBeInstanceOf(Date) for rows[0].open and rows[0].close; strengthen it by
asserting exact UTC times to catch time-shift regressions: after fetching rows
with client.tradingHour.findMany, compare rows[0].open and rows[0].close (and
any other relevant rows) against the expected UTC values (e.g., via
toISOString() or constructing new Date(Date.UTC(...))) so the assertions verify
the precise UTC timestamp in addition to being Date instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2a7f3c9-ae3a-4067-b9bf-c85d4a0ad068

📥 Commits

Reviewing files that changed from the base of the PR and between a31a32e and 437018e.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/postgresql.ts
  • packages/orm/src/client/crud/operations/base.ts
  • tests/regression/test/issue-2633.test.ts

* full ISO timestamps).
*/
transformInput(value: unknown, _type: BuiltinType, _forArrayField: boolean) {
transformInput(value: unknown, _type: BuiltinType, _forArrayField: boolean, _fieldDef?: FieldDef) {
Copy link
Copy Markdown
Contributor Author

@erwan-joly erwan-joly Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very close to what we initially did and reverted on a previous PR — but I don't think there's a better way to solve this here. The shape-detect trick from #2590 worked for the read side because PG emits time-only values (09:30:00) and datetimes (2026-04-29 ...) with distinguishable prefixes. On the write side the input is always a Date object, which carries no shape clue about whether the destination column is time or timestamp, so we genuinely need the field metadata to format correctly. Open to alternatives if you see one.

Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
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.

ZenStack 3.6: writes to @db.Time columns fail with PG 22007 (Date serialized as ISO datetime)

1 participant