Files
jellybit/openspec/changes/archive/2026-07-04-time-storage-rfc3339/proposal.md
T
avandClaude Opus 4.8 5d5456fa68 Хранение времени: RFC 3339 (UTC) + таймзона отображения в конфиге
Метки времени в SQLite переведены с формата datetime('now')
(«2006-01-02 15:04:05») на RFC 3339 всегда-UTC («2006-01-02T15:04:05Z»):
самоописываемое хранилище (зона в значении), валидный ISO 8601, единый
формат с логами. Фиксированная ширина сохраняет лексикографическую
сортировку TEXT = хронологию (COALESCE(source_added_at, created_at)).

- Единая точка генерации времени в Go: store.Now()/FormatTime; DEFAULT
  (datetime('now')) снят со всех колонок — время всегда пишет приложение
  (зеркально ident.NewID для id), fail-loud при забытой вставке (NOT NULL).
  Все INSERT-сайты в store передают created_at/updated_at явно.
- Миграция 0008 (rebuild 7 таблиц без DEFAULT + backfill strftime, FK/PK/
  индексы сохранены байт-в-байт по образцу 0006); симметричная down.
- Новая секция конфига [general] с полем timezone (дефолт UTC) — зона
  ОТОБРАЖЕНИЯ в веб-UI; хранение остаётся UTC. Жёсткая валидация зоны на
  старте; zoneinfo встроен (time/tzdata), заменён зашитый Europe/Moscow.
- Тесты: round-trip миграции (up/down, NULL source_added_at), валидация
  зоны, сдвиг даты по зоне; обновлены фикстуры и TestUlidMigration.
- Docs: конвенции database/config, ER-схема; спека web-ui (таймзона).

OpenSpec change time-storage-rfc3339 (заархивирован).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:32:07 +03:00

4.5 KiB
Raw Blame History

Why

Временные метки в БД хранятся как TEXT в формате datetime('now')2006-01-02 15:04:05: пробел вместо T, без суффикса зоны. UTC гарантируется только конвенцией и кодом, но не самим значением — тот, кто откроет базу напрямую (sqlite3 CLI, дамп, сторонний инструмент), не видит зону, а строка не является валидным ISO 8601 (footgun для new Date() в веб-UI, требует переформатирования для JSON API / <time datetime>). Одновременно зона отображения зашита в код (Europe/Moscow), хотя серверы часто настроены по UTC.

What Changes

  • Формат хранения меток времени переходит на RFC 3339, всегда UTC с суффиксом Z (2006-01-02T15:04:05Z). Секундная точность сохраняется. Фиксированная ширина сохраняет лексикографическую сортировку TEXT = хронологию.
  • Единая точка генерации времени в Go (зеркально принципу «ULID генерит только internal/ident»): DEFAULT (datetime('now')) убирается со всех колонок; метки времени всегда пишет приложение через store.FormatTime.
  • Go-хелперы FormatTime/ParseTime переводятся на stdlib time.RFC3339.
  • BREAKING (хранение): одноразовая goose-миграция пересобирает таблицы без DEFAULT и backfill-ит существующие строки в новый формат. Backfill + смена схемы + смена кода — один change, чтобы не возникло окна со смешанными форматами (иначе ломается лексикографическая сортировка COALESCE).
  • Добавляется секция конфига [general] (общие настройки) с явной настройкой таймзоны отображения (timezone, дефолт UTC), валидируемой на старте через time.LoadLocation. Хранение остаётся всегда UTC — настройка влияет только на рендеринг времени в веб-UI, заменяя зашитый Europe/Moscow.

Capabilities

New Capabilities

Нет. Формат хранения времени — кросс-каттинг конвенция (how we store), по правилам проекта живёт в docs/conventions/database.md, а не в OpenSpec.

Modified Capabilities

  • web-ui: временные метки в интерфейсе SHALL отображаться в таймзоне из конфига ([general].timezone, дефолт UTC), а не в жёстко зашитой Europe/Moscow.

Impact

  • Код: internal/store/download.go (sqliteTimeLayouttime.RFC3339, FormatTime/ParseTime, инлайн updated_at = datetime('now')); Create-методы store (download.go, recognition.go) — все INSERT, полагавшиеся на DEFAULT, теперь явно передают created_at/updated_at; новый store.Now() как единая тестируемая точка.
  • Миграции: новая goose-миграция — rebuild 7 таблиц без DEFAULT + backfill колонок created_at/updated_at/source_added_at.
  • Конфиг: новая секция [general] с полем timezone в internal/config, жёсткая валидация на старте, проброс зоны в internal/httpapi (замена mskLoc в live.go). Встраивание zoneinfo — import _ "time/tzdata" в cmd/jellybit (устраняет latent silent-UTC на контейнере без tzdata; +~450КБ к бинарю).
  • Docs: docs/conventions/database.md, docs/conventions/config.md, docs/specs/database.md (комментарий формата в ER-схеме).
  • Совместимость: ULID-корреляция и хронология сохраняются (значения меток эквивалентны по моменту); секунды не теряются.