Skip to content

fix: write and compare sqlite index timestamps in UTC (#1497) - #1498

Merged
v1r3n merged 6 commits into
mainfrom
fix/sqlite-index-utc-timestamps
Aug 7, 2026
Merged

fix: write and compare sqlite index timestamps in UTC (#1497)#1498
v1r3n merged 6 commits into
mainfrom
fix/sqlite-index-utc-timestamps

Conversation

@mp-orkes

@mp-orkes mp-orkes commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #1497 - take a look at the screen recording for a repro of the issue.

What changed

SqliteIndexDAO wrote start_time/update_time via java.sql.Timestamp.toString(), which renders in the JVM default zone, while SqliteIndexQueryBuilder rendered the search bound in UTC. These columns are TEXT-compared, so every time-range search was wrong by the host's UTC offset.

Both sides now use one canonical format, yyyy-MM-dd HH:mm:ss.SSS in UTC, applied to all four index timestamp columns (workflow_index and task_index × start_time/update_time).

V6__index_timestamps_to_utc.sql converts existing rows.

How to test

./gradlew :conductor-sqlite-persistence:test

The suite runs under TZ=America/Los_Angeles (set in sqlite-persistence/build.gradle). This is deliberate and load-bearing — CI runs UTC, where a local-time/UTC mismatch is invisible and every one of these tests passes vacuously.

To see it red, check out the first commit alone (tests without the fix): 15 failures, including the reported symptom as expected:<1> but was:<0>..

🤖 Generated with Claude Code

mp-orkes and others added 4 commits August 7, 2026 00:05
…#1497)

SqliteIndexDAO writes start_time/update_time via Timestamp.toString(), which
renders in the JVM default zone, while SqliteIndexQueryBuilder renders search
bounds in UTC. On a negative-offset host this makes recent rows invisible to
time-range searches.

- Pin the sqlite-persistence test task to TZ=America/Asuncion so the
  regression is visible; it's a no-op on UTC CI otherwise.
- SqliteIndexDAOTest: add searchesFindWorkflowsIndexedInANonUtcZone and
  searchesFindTasksIndexedInANonUtcZone (currently 0 hits instead of 1), and
  switch the stored-text assertions in compareWorkflowSummary/
  compareTaskSummary from Timestamp.toString() to the canonical UTC format
  the fix will use.
- SqliteIndexQueryBuilderTest: add a test asserting the datetime() wrapper is
  dropped and the bound parameter is rendered in canonical UTC text.
- Add SqliteIndexTimestampMigrationTest, which loads the not-yet-created
  V6__index_timestamps_to_utc.sql from the classpath and fails with an
  explicit "migration file not found" message until that migration ships.

All 15 new/changed assertions are red for the right reason (timezone/format
mismatch or missing migration file), not compile errors.
SqliteIndexDAO wrote start_time/update_time via java.sql.Timestamp.toString(),
which renders in the JVM default zone, while SqliteIndexQueryBuilder rendered
search bounds in UTC. Because the columns are TEXT-compared, every time-range
search was off by the host's UTC offset (on America/Asuncion, -4h).

Write and read paths now share a canonical yyyy-MM-dd HH:mm:ss.SSS UTC format
that is byte-identical to SQLite's strftime('%Y-%m-%d %H:%M:%f', ...). Also
drop the datetime() wrapper in the read path, since it truncated to whole
seconds and could wrongly exclude rows with a fractional-second lower bound.
A new V6 migration rewrites existing rows to the canonical UTC text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The zone only needs to be non-UTC with DST for these regression tests to be
meaningful. Asuncion was the reporting host's zone, which is needless local
color in a shared build file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The byte-for-byte rationale is already stated on toSqliteUtcTimestamp and in
the V6 migration; repeating it over each field constant added noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mp-orkes
mp-orkes force-pushed the fix/sqlite-index-utc-timestamps branch from 63f3720 to 9c36c68 Compare August 7, 2026 03:07
@mp-orkes mp-orkes self-assigned this Aug 7, 2026
…ne (#1497)

The V6 migration used SQLite's 'utc' modifier to reinterpret the stored
local-time text. That reads the host tz database, but the bad values were
written against the JVM's bundled tz database, and the two disagree whenever
the JVM's tzdata is older than the OS's.

Observed on JDK 21 (tzdata 2023c) against macOS (tzdata 2026c): Paraguay
dropped DST in tzdata 2024b, so the JVM wrote America/Asuncion at -04:00 while
SQLite read it back at -03:00. Every migrated row landed an hour early, which
is enough to hide recent executions from a "last 30 minutes" search.

start_time/update_time are derived data: json_data already carries the
authoritative instant as an ISO-8601 string that SQLite parses natively. Rebuild
the columns from it and no timezone is consulted at any point. This is exact
under tzdata skew, exact when a DB moves between hosts in different zones (a
limitation the previous version documented as unfixable), and self-correcting
for rows an earlier attempt already shifted wrongly.

Adds a json_valid() guard: json_extract() raises "malformed JSON" and aborts the
whole statement, which would have failed the migration and stopped the server
from booting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mp-orkes

mp-orkes commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

This is happening with SQLite.
On this screen recording you can see that I run a workflow, then search within last 15min, last 30min... and get nothing until last 12h. Which is wrong.

sqlite-tz-issue.mp4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mp-orkes
mp-orkes marked this pull request as ready for review August 7, 2026 03:47
@v1r3n
v1r3n merged commit dc358c4 into main Aug 7, 2026
13 checks passed
@v1r3n
v1r3n deleted the fix/sqlite-index-utc-timestamps branch August 7, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite index stores start_time in local time but searches it as UTC — time-range filters hide recent executions

2 participants