Skip to content

driver-memory analytics dateRange upper bound is INCLUSIVE for a full-timestamp end, so the first instant of tomorrow counts as today #16179

Description

@claude

Found while working on #16042 (the AnalyticsQuery.timezone repair in the same function). ⛔ Deliberately NOT fixed there — that card's fence was WHICH calendar the window is anchored to; this is the window's upper-bound OPERATOR, a pre-existing question in both calendars. Refs #16042, #15825.

The shape

packages/drivers/driver-memory/src/memory-analytics.ts:760 widens a dateRange end with nextUtcCalendarDay(end):

const nextDay = nextUtcCalendarDay(end);
const stringBounds = nextDay != null
  ? { $gte: start, $lt: nextDay }      // bare `YYYY-MM-DD` end -> half-open, correct
  : { $gte: start, $lte: end };        // anything else -> INCLUSIVE

nextUtcCalendarDay widens only a bare YYYY-MM-DD string and returns null for a full ISO timestamp — that is its documented contract (packages/spec/src/data/calendar-day.ts:42, pinned by calendar-day.test.ts:29-31). So every end that is a timestamp rather than a bare day falls to the $lte branch and the window becomes closed at both ends.

Measured through the public entry, against the BUILT package

Explicit array bounds, so no relative-token resolution is involved:

rows:  2026-09-06T00:00:00.000Z, 2026-09-06T12:00:00.000Z, 2026-09-07T00:00:00.000Z
query: timeDimensions: [{ dimension: 'events.createdAt',
                          dateRange: ['2026-09-06T00:00:00.000Z','2026-09-07T00:00:00.000Z'] }]

selected: ALL THREE, including 2026-09-07T00:00:00.000Z

⇒ the instant that begins the NEXT day is counted inside the window that ends there.

Why it matters beyond an explicit array

parseDateRangeString('today') returns exactly such a pair — the day's start instant and the next day's start instant. So dateRange: 'today' counts a row stamped at precisely 00:00:00.000 of tomorrow. Two adjacent day windows therefore overlap at one instant, and a row landing on it is counted in both. It is a one-instant error, but it is a silent double-count, and a CURRENT_TIMESTAMP-written row truncated to the second is not as improbable as a millisecond-precision one.

This is calendar-independent: it was true when the window was the UTC day and it is still true now that a supplied timezone moves the window (#16042), because the operator choice never depended on which calendar produced the bounds.

The decision it needs

The comment at the call site states the intent — "Both spellings are half-open on a bare-day end (#4042; the SQL twin is #3777)" — so half-open is clearly the design for the bare-day case. The question is whether a full-timestamp end should be half-open too:

  1. Half-open everywhere ($lt on any end): makes 'today' exactly one day and removes the overlap; but it silently narrows an explicit dateRange: [a, b] that a caller wrote meaning "inclusive of b", which is the reading $lte gives today.
  2. Keep $lte for an explicit timestamp end, make the relative tokens emit a bare-day or an exclusive end: fixes the token path without changing what an explicit array means.

⇒ Contract-first, so filing rather than patching. ⚠️ Related but distinct from #16041, which is about an unparseable dateRange matching every row; this one is about a parsed window's boundary.

Unlabelled on purpose: routing, domain:*, type and priority belong to triage.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions