Skip to content

fix(oracle): read DATE and TIMESTAMP as the stored wall clock, not a local Date - #1225

Merged
cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/1131-oracle-date-timezone
Oct 2, 2026
Merged

cevheri merged 2 commits into
libredb:mainfrom
Aditya-XR:fix/1131-oracle-date-timezone

Conversation

@Aditya-XR

@Aditya-XR Aditya-XR commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Description

The Oracle provider now returns DATE and TIMESTAMP (without time zone) as the engine's own wall clock, 2026-09-01 00:00:00 / 2026-09-01 10:30:00.345, whatever time zone the Studio server process runs in. The SQL INSERT export writes that text back through TO_DATE / TO_TIMESTAMP.

oracledb (Thin and Thick) builds both types as new Date(y, m - 1, d, h, mi, s, ms) in the Node process's zone (makeDate(useLocal) in lib/util.js), and every row path serialized it with toISOString. A converter in the per-call fetch type handler of query() and queryInTransaction() reads the Date back through its local getters, which is the exact inverse. As found in the issue, fetching as STRING does not work in Thin mode, and moving the session TIME_ZONE would change CURRENT_DATE / LOCALTIMESTAMP in users' own queries. TIMESTAMP WITH TIME ZONE and WITH LOCAL TIME ZONE are untouched, and readDefinition() keeps the LOB-only handler.

Library surface: in-process consumers now receive strings instead of Date objects for these two Oracle types, the same as #1134 did for PostgreSQL.

Where to look

The behaviour change is about 110 lines in three files: rowFetchTypeHandler / formatZonelessDate in oracle.ts, oracleTextLiteral / oracleDateColumn in result-export.ts, and two type additions in db-drivers.d.ts. The rest is tests (≈460 lines, including the live script) and docs (≈110 lines).

Live measurement

tests/live/oracle-zoneless-values.ts, 2026-10-02, gvenzl/oracle-free:slim (Oracle AI Database 26ai Free Release 23.26.3.0.0), oracledb 6.10.0, Bun 1.4.2. Every value read through the provider is compared with the server's own TO_CHAR:

process TZ DATE before TIMESTAMP before DATE now TIMESTAMP now
UTC 2026-09-01T00:00:00.000Z 2026-09-01T10:30:00.000Z 2026-09-01 00:00:00 2026-09-01 10:30:00
Europe/Istanbul 2026-08-31T21:00:00.000Z 2026-09-01T07:30:00.000Z 2026-09-01 00:00:00 2026-09-01 10:30:00
America/Los_Angeles 2026-09-01T07:00:00.000Z 2026-09-01T17:30:00.000Z 2026-09-01 00:00:00 2026-09-01 10:30:00
  • The other rows also match TO_CHAR in all three zones: 10:11:12 on a DATE, .345 and .5 fractions, NULL, and the BC date -0044-03-15 00:00:00. Before the change, that BC date read as -000044-03-14T22:04:08.000Z under Istanbul.
  • TIMESTAMP WITH TIME ZONE (the control) reads 2026-09-01T07:30:00.000Z in every zone, before and after.
  • Export, run under Istanbul over JSON as POST /api/db/query hands it on. Before: VALUES (1, '2026-08-31T21:00:00.000Z', '2026-09-01T07:30:00.000Z'), refused on replay with ORA-01861: literal does not match format string. Now: TO_DATE('2026-09-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'), TO_TIMESTAMP('2026-09-01 10:30:00.345', 'YYYY-MM-DD HH24:MI:SS.FF') and SYYYY for the BC row. All four rows replay to values the server compares as equal.
  • The script also checks that the raw driver DATE under Istanbul is still the shifted 2026-08-31T21:00:00.000Z. If the driver changes that, the guard fails instead of silently testing nothing.

One correction to the issue text: in my measurement the old in-process export (rows still holding the driver's Date) replayed correctly, because oracleDateLiteral already used the local getters. The broken path was HTTP, where it was refused rather than writing the wrong day. The docs say this.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test addition or update

Related Issue

Closes #1131

Changes Made

  • src/lib/db/providers/sql/oracle.ts: formatZonelessDate and rowFetchTypeHandler (DATE/TIMESTAMP converter, then the existing LOB mapping), used by query() and queryInTransaction().
  • src/lib/export/result-export.ts: for a column declared DATE or TIMESTAMP[(n)], text in exactly the provider's form becomes TO_DATE / TO_TIMESTAMP with a mask matching that text (SYYYY only for a BC year, .FF only when there is a fraction). A VARCHAR2 with the same characters, or any other text, stays quoted. The Date path is unchanged for hosts that build their own rows.
  • src/types/db-drivers.d.ts: DB_TYPE_DATE, DB_TYPE_TIMESTAMP, and the { converter } form of FetchType.
  • tests/integration/db/oracle-provider.test.ts: 9 tests. The same row is read under UTC, Europe/Istanbul and America/Los_Angeles through a mock execute() that applies the handler as the driver does. They cover the fraction, padding, BC year, NULL, declared types, the SQL INSERT export of the read, queryInTransaction(), and the two zoned types as unconverted controls.
  • tests/unit/lib/export/result-export.test.ts: 6 tests for the text literals and the cases that must stay quoted.
  • tests/live/oracle-zoneless-values.ts: the opt-in live guard above, modelled on mssql-zoneless-values.ts.
  • docs/providers/oracle.md: §5.3 value table and a new subsection with the measurements, the §5.5 export table, §12 coverage and §12.4 live script, and the three driver-side losses in §14.

Testing

  • I have tested this locally
  • I have added/updated tests
  • All existing tests pass

11 of the 15 new tests fail on main and pass with the change; the other four are the controls (zoned types, declared column types, a VARCHAR2 and malformed text staying quoted). oracle-provider.test.ts 228 pass and result-export.test.ts 113 pass through the runner. format, lint (0 errors, no new warnings), typecheck, knip, the four drift guards, build, build:lib and attw all pass.

In the full bun run test on Windows, the only failures are the node:sqlite driver test, launcher-utils and pack-standalone-tarball. They fail the same way on main on Windows.

Test Environment

  • LibreDB Studio Version: 0.17.0 (main at 4b6aae8)
  • Browser: N/A (provider and export change)
  • OS: Windows 11
  • Node.js/Bun Version: Node 22.17.1 / Bun 1.4.2
  • Database Type: Oracle AI Database 26ai Free 23.26.3.0.0 (gvenzl/oracle-free:slim), oracledb 6.10.0

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The required CI test job passes the 100% line-coverage gate (bun run test:coverage and bun run coverage:check)
  • If I changed src/lib/db/providers/, I updated the matching docs/providers/ documentation and tests/integration/db/ tests in the same PR (provider triad)
  • Any dependent changes have been merged and published

Additional Notes

About the coverage box: on Windows, test:coverage goes red on the Windows-only failures above and so skips the merge. Merging the 694 per-file reports by hand and running scripts/check-coverage.mjs on the result gives 71725/71748 lines. The 23 uncovered lines are src/lib/db/providers/sql/sqlite.ts (the failing node:sqlite test) and src/instrumentation.ts 32-36 (a POSIX-only test). oracle.ts and result-export.ts are fully covered. On CI's Linux runner the gate passes: check-coverage: OK — 71748/71748 lines (100.00%) in the Unit & Integration Tests job.

The separate TIMESTAMP WITH TIME ZONE export-over-HTTP finding is filed as #1224.

…local Date

oracledb builds both types as a Date in the Node process's zone, and every
row path serialized it as ISO UTC, so DATE '2026-09-01' read as
2026-08-31T21:00:00.000Z under TZ=Europe/Istanbul. The row fetch type
handler now converts both through the Date's local getters, the inverse of
the driver's makeDate(useLocal), and the SQL INSERT export writes that text
back through TO_DATE / TO_TIMESTAMP for a column declared DATE or
TIMESTAMP[(n)]. TIMESTAMP WITH [LOCAL] TIME ZONE stay instants.

Adds tests/live/oracle-zoneless-values.ts, which compares the provider's
read with the server's own TO_CHAR under UTC, Europe/Istanbul and
America/Los_Angeles and replays the export into a copy table.

Closes libredb#1131
Copilot AI balanced review requested due to automatic review settings October 1, 2026 22:00

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri

cevheri commented Oct 2, 2026

Copy link
Copy Markdown
Member

Thanks, this is ready and I will merge it shortly.

With main's oracle.ts and result-export.ts put back, 11 of your new tests fail, which matches your count. Breaking the converter in a dozen small ways, such as UTC getters, no year padding, a lost BC sign, an untrimmed fraction, or queryInTransaction left on the LOB handler, turns at least one case red each time. Both files still pass on top of current main, which has moved since you branched. Your Thin-mode finding on the issue is what made this approach work.

One thing I will add on top: oracleTextLiteral puts the cell text into TO_DATE without quoting, so the ^ and $ of ORACLE_ZONELESS_TEXT are what keep the export safe. They hold today, but every test stays green with either one removed. I will pin them with a case that has text after a valid timestamp and one with text before it.

…#1131)

The provider's DATE and TIMESTAMP text goes into TO_DATE / TO_TIMESTAMP
unquoted, so ORACLE_ZONELESS_TEXT has to match the whole cell. Removing
either ^ or $ kept every test green; a wall clock with text before or
after it now has to stay an escaped quoted literal.
@cevheri
cevheri merged commit 14728c7 into libredb:main Oct 2, 2026
46 of 48 checks passed
@Aditya-XR
Aditya-XR deleted the fix/1131-oracle-date-timezone branch October 3, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oracle DATE and TIMESTAMP values shift with the server process's time zone

3 participants