Skip to content

perf(postgres-driver): Fast date, timestamp/tz parsers#10737

Merged
ovr merged 7 commits intomasterfrom
perf/postgres-driver-optimize-date-time-parsing
Apr 23, 2026
Merged

perf(postgres-driver): Fast date, timestamp/tz parsers#10737
ovr merged 7 commits intomasterfrom
perf/postgres-driver-optimize-date-time-parsing

Conversation

@ovr
Copy link
Copy Markdown
Member

@ovr ovr commented Apr 23, 2026

getTypeParser routed all three timestamp OIDs through a single moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS) call, which was the dominant per-row cost for result sets with any timestamp columns. Each OID has a fixed text shape, so we can parse directly:

  • OID 1082 (DATE): single template literal appending T00:00:00.000.
  • OID 1114 (TIMESTAMP): slice + pad / truncate fractional, no TZ scan.
  • OID 1184 (TIMESTAMPTZ): the driver pins session TZ to UTC by default, so Postgres emits +00 on the wire — a fast path detects all-zero offsets, strips the suffix, and delegates to the TIMESTAMP parser. Non-UTC sessions fall through to Date.UTC + hand-rolled pad2/pad3/pad4 formatting, with setUTCFullYear handling the Date.UTC "years 0-99 remapped to 1900+year" legacy quirk for pre-100 AD dates.

Microbenchmark (Node 22, Apple Silicon; moment baseline = moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS)).

Warm (N=2M, with warmup)

Steady-state throughput after V8's JIT has fully optimized both paths — representative of large result sets.

input moment new speedup
DATE ~2770 ns ~6 ns ~450x
TIMESTAMP (no frac) ~4440 ns ~28 ns ~160x
TIMESTAMP (ms) ~4970 ns ~48 ns ~105x
TIMESTAMP (us → ms trunc) ~4920 ns ~41 ns ~120x
TIMESTAMPTZ (+00) ~4850 ns ~35 ns ~135x
TIMESTAMPTZ (ms +00) ~5360 ns ~57 ns ~95x
TIMESTAMPTZ (us +00) ~5250 ns ~62 ns ~85x
TIMESTAMPTZ (+02) (non-UTC session) ~4690 ns ~390 ns ~12x
TIMESTAMPTZ (ms +05:30) (non-UTC) ~5350 ns ~435 ns ~12x
TIMESTAMPTZ (us -03) (non-UTC) ~5200 ns ~400 ns ~13x

Cold (N=100, no warmup)

No warmup, fresh module cache — representative of the first few rows / short-lived queries. moment sits at ~15 µs/call cold (its locale tables and regex compilation happen lazily on first use), which is where the biggest absolute wins live.

input moment new speedup
DATE ~13.8 µs ~35 ns ~390x
TIMESTAMP (no frac) ~18.5 µs ~139 ns ~135x
TIMESTAMP (ms) ~16.0 µs ~135 ns ~120x
TIMESTAMP (us) ~16.2 µs ~128 ns ~125x
TIMESTAMPTZ (+00) ~16.1 µs ~1230 ns ~13x
TIMESTAMPTZ (ms +00) ~16.5 µs ~290 ns ~55x
TIMESTAMPTZ (us +00) ~15.2 µs ~390 ns ~40x
TIMESTAMPTZ (+02) (non-UTC) ~14.7 µs ~880 ns ~17x
TIMESTAMPTZ (ms +05:30) (non-UTC) ~15.3 µs ~1000 ns ~15x
TIMESTAMPTZ (us -03) (non-UTC) ~16.7 µs ~1000 ns ~17x

Real showcase of the problem, parsing dates tooks ~10% of the whole request, spending 390ms.

image

@ovr ovr requested a review from a team as a code owner April 23, 2026 11:52
@github-actions github-actions Bot added driver:postgres Issues relating to the Postgres driver javascript Pull requests that update Javascript code data source driver labels Apr 23, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 23, 2026

Claude finished @ovr's task in 2m 19s —— View job


Code Review: perf(postgres-driver) — Fast date/timestamp parsers

  • Read and analyze all changed files
  • Review code quality and correctness
  • Review edge case handling and security
  • Review performance considerations
  • Review test coverage
  • Post final review

Overall: This is a clean, well-motivated performance optimization. The implementation is correct and the approach is sound. Replacing moment.utc().format() with purpose-built parsers for fixed-format Postgres wire output is the right call — moment was massive overkill for deterministic string shapes.

Correctness

The three parsers produce output identical to the old moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS) for all practical inputs. I verified the sign/offset logic in timestampTzTypeParser manually:

  • +02Date.UTC(...) - positive_offset → correctly shifts forward time back to UTC ✓
  • -05Date.UTC(...) - negative_offset → correctly shifts behind time forward to UTC ✓

The fractional-second pad/truncate trick (${val.slice(20, 23)}00.slice(0, 3)`) is clever and handles 1–6 fractional digits correctly.

Subclass Impact

CrateDriver, RedshiftDriver, and MaterializeDriver all extend PostgresDriver and inherit getTypeParser without overriding it. Since the output format is identical, this is a transparent upgrade for all four drivers.

Performance

The getTypeParser dispatch was changed from Array.includes() (linear scan of a 3-element array) to direct === comparisons, which is a nice micro-optimization on top of the parser rewrite. Using Date.UTC() + hand-rolled formatters for TIMESTAMPTZ is pragmatic — full manual UTC arithmetic (carrying days/months/years across boundaries) would be complex and error-prone for diminishing returns.

Items to Consider

I left three inline comments:

  1. Missing test for HH:MM:SS timezone offset — the timestampTzTypeParser handles +HH:MM:SS offsets (lines 57–61 in type-parsers.ts) but no test exercises this branch.

  2. Benchmark requires moment which is no longer a dependencybench-timestampTypeParser.js imports moment, but it was removed from dependencies. Adding it to devDependencies would keep the bench runnable without polluting the published package.

  3. Year padding for exotic datesgetUTCFullYear() isn't zero-padded, so years < 1000 would produce fewer than 4 digits. Extremely unlikely in practice, but noted for completeness.

Summary

This is a high-quality PR — well-scoped, well-tested, well-benchmarked. The issues above are all minor/nit-level. Nice win dropping the moment dependency from this package entirely.

Comment thread packages/cubejs-postgres-driver/src/type-parsers.ts
Comment thread packages/cubejs-postgres-driver/src/type-parsers.ts Outdated
Comment thread packages/cubejs-postgres-driver/bench-timestampTypeParser.js Outdated
ovr added 3 commits April 23, 2026 13:57
`getTypeParser` routed all three timestamp OIDs through a single
`moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS)` call,
which was the dominant per-row cost for result sets with any timestamp
columns. Each OID has a fixed text shape, so we can parse directly:

- OID 1082 (DATE): single template literal appending `T00:00:00.000`.
- OID 1114 (TIMESTAMP): slice + pad/truncate fractional, no TZ scan.
- OID 1184 (TIMESTAMPTZ): parse fields, `Date.UTC` for the offset shift,
  hand-rolled `pad2`/`pad3` for formatting (padStart measured ~15-20%
  slower in the hot path).

Microbenchmark (2M iters/variant, Node, `bench-timestampTypeParser.js`):

| input                      | moment  | new    | speedup |
| -------------------------- | ------- | ------ | ------- |
| DATE                       | ~2700ns | ~7ns   | ~400x   |
| TIMESTAMP (no frac)        | ~4300ns | ~24ns  | ~180x   |
| TIMESTAMP (ms)             | ~4800ns | ~41ns  | ~115x   |
| TIMESTAMP (us -> ms trunc) | ~4900ns | ~40ns  | ~120x   |
| TIMESTAMPTZ (+02)          | ~4700ns | ~360ns | ~13x    |
| TIMESTAMPTZ (ms +05:30)    | ~5500ns | ~480ns | ~11x    |
| TIMESTAMPTZ (us -03)       | ~5300ns | ~400ns | ~13x    |
@ovr ovr force-pushed the perf/postgres-driver-optimize-date-time-parsing branch from 04e1852 to 1af79bc Compare April 23, 2026 12:00
@ovr ovr force-pushed the perf/postgres-driver-optimize-date-time-parsing branch from 45cf24d to 39393cf Compare April 23, 2026 12:09
@ovr ovr force-pushed the perf/postgres-driver-optimize-date-time-parsing branch from 39393cf to 941e778 Compare April 23, 2026 12:11
@ovr ovr changed the title perf(postgres-driver): Fast date, timestamp/tz parsers, ~180x perf(postgres-driver): Fast date, timestamp/tz parsers, ~115x Apr 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.02%. Comparing base (7f1ec2e) to head (d9d7a08).
⚠️ Report is 1 commits behind head on master.

❗ There is a different number of reports uploaded between BASE (7f1ec2e) and HEAD (d9d7a08). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (7f1ec2e) HEAD (d9d7a08)
cubesql 1 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #10737       +/-   ##
===========================================
- Coverage   83.40%   58.02%   -25.39%     
===========================================
  Files         251      215       -36     
  Lines       75297    16689    -58608     
  Branches        0     3358     +3358     
===========================================
- Hits        62802     9683    -53119     
+ Misses      12495     6514     -5981     
- Partials        0      492      +492     
Flag Coverage Δ
cube-backend 58.02% <ø> (?)
cubesql ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ovr ovr changed the title perf(postgres-driver): Fast date, timestamp/tz parsers, ~115x perf(postgres-driver): Fast date, timestamp/tz parsers Apr 23, 2026
@ovr ovr merged commit c3a5709 into master Apr 23, 2026
77 of 78 checks passed
@ovr ovr deleted the perf/postgres-driver-optimize-date-time-parsing branch April 23, 2026 13:44
Copy link
Copy Markdown
Contributor

@KSDaemon KSDaemon left a comment

Choose a reason for hiding this comment

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

This is awesome!👍🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data source driver driver:postgres Issues relating to the Postgres driver javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants