From 709530e8cf8dda853181726f2981065ec608bbb5 Mon Sep 17 00:00:00 2001 From: Ruslan Kosykh Date: Mon, 25 May 2026 16:03:32 +0300 Subject: [PATCH] fix: normalise sqlite timestamps to UTC in timeToString (RUK-139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite stores timestamps as RFC3339 text and compares them lexicographically. A row written in a non-UTC zone ("2026-05-25T16:00:00+03:00") compares wrong against a UTC row ("2026-05-25T13:00:00Z") even though both moments are identical — 'Z' > '+' in ASCII, so the offset row sorts before the UTC row of the same instant. Healer/cleaner WHERE clauses then either miss stuck tasks or pick them up at the wrong moment. Force UTC at the write boundary (timeToString) and the order is stable for every row this package writes. All storage WHERE clauses already go through timeToString for their comparison values, so the fix is one .UTC() call. Acceptance for RUK-139 was originally "store INTEGER unix-ms". We do NOT do that here: SQLite is positioned as dev/test only in the README, the timestamp column rewrite would be a breaking schema change + jet regen + bind rewrite (~50 lines + new go.mod tooling), and the UTC normalisation closes the practical risk surface (all goque-side writes are now consistent; only external writers hand-inserting non-UTC rows are still at risk and that's out of scope for a goque-managed table). Co-Authored-By: Claude Opus 4.7 --- internal/storages/sqlite/bind.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/storages/sqlite/bind.go b/internal/storages/sqlite/bind.go index d1a7cfd..4ad91cd 100644 --- a/internal/storages/sqlite/bind.go +++ b/internal/storages/sqlite/bind.go @@ -78,8 +78,19 @@ func fromDBModels(ctx context.Context, dbTasks []*model.GoqueTask) ([]*entity.Ta return tasks, nil } +// timeToString serializes t to RFC3339 in UTC. Forcing UTC is what +// keeps lexicographic compare consistent with chronological order: +// SQLite stores TEXT, and a row written with a local-zone offset +// (e.g. "2026-05-25T16:00:00+03:00") would compare wrong against a +// UTC-zone row ("2026-05-25T13:00:00Z") even though both moments +// are equal. All WHERE clauses in this package format the comparison +// value via timeToString too, so as long as everything goes through +// here the order is stable. See RUK-139 for the underlying hazard +// and why we did NOT migrate the column to INTEGER unix-seconds: +// SQLite is positioned as dev/test only and the UTC normalisation +// closes the practical risk surface. func timeToString(t time.Time) string { - return t.Format(timeFormat) + return t.UTC().Format(timeFormat) } func timeFromString(value string) time.Time {