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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
ymc9 merged 2 commits into
zenstackhq:devfrom
erwan-joly:fix/2633-time-column-write-serialization
May 8, 2026
Merged

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

Conversation

@erwan-joly

@erwan-jolyerwan-joly commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

modelTradingHour {idInt@id@default(autoincrement())openDateTime@db.Time(6)closeDateTime@db.Time(6)}
// 3.6.4 — throws PG 22007// this PR — worksawaitdb.tradingHour.create({data: {open: newDate('1970-01-01T09:00:00.000Z'),close: newDate('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.
Fixeszenstackhq#2633
@coderabbitai

coderabbitaiBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically describes the main change: fixing Date formatting for @db.Time/@db.Timetz columns in PostgreSQL dialect.
Linked Issues check✅ PassedAll 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✅ PassedAll 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✅ PassedNo 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Addresses CodeRabbit nitpick: toBeInstanceOf(Date) alone does not
catch time-shift regressions. Add toISOString() comparisons against
the expected anchored UTC values for both rows.
ymc9
ymc9 approved these changes May 8, 2026

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

I don't have a better solution than inspecting the @db.Time attribute 😂. Time may deserve a built-in type for cleaner handling, but that's gonna be a bigger change and probably not worth it yet.

@ymc9
ymc9 merged commit d1db37c into zenstackhq:devMay 8, 2026
9 checks passed
Sign up for freeto 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)

2 participants

@erwan-joly@ymc9