Метки времени в 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>
5.8 KiB
5.8 KiB
1. Ревью дизайна (чекпоинт ДО кода)
- 1.1 Согласовать design.md: стратегию миграции/backfill, снятие
DEFAULT, секцию[general]сtimezone. Правки внести до реализации.
2. Хелперы формата времени (store)
- 2.1
internal/store/download.go: убратьsqliteTimeLayout;FormatTime→t.UTC().Format(time.RFC3339),ParseTime→time.Parse(time.RFC3339, s). Обновить комментарии (формат...Z, неdatetime('now')), включая коммент проSourceAddedAtнаdownload.go:118(ссылается наsqliteTimeLayout). - 2.2 Добавить
store.Now()(обёртка надtime.Now().UTC()) как единую точку получения «сейчас» в слое store. - 2.3 Проверить
migrations/0006_ulid_identity.go:204(parseCreatedAtпарсит старый формат) — это историческая миграция, формат менять не нужно; убедиться, что она не ломается и не пересекается с новой.
3. Убрать DEFAULT и писать время из Go
- 3.1 Каждый INSERT-сайт со временем явно передаёт
created_at/updated_at(=FormatTime(store.Now())). Полный список сайтов (не методов!):download.go—CreateDownloadIfNoActive(вставка download :219 + вставка хешей :228), dedup top-up хешей :203 (INSERT OR IGNORE),AddInfohashes:306 (INSERT OR IGNORE);recognition.go—CreateRecognition:72,AddHint:108,SetOverride:130,AddFileLink:185,AddMetadataCandidate:332. ДляINSERT OR IGNOREвdownload_infohashcreated_atобязателен на новой строке (после снятияDEFAULT+NOT NULL); при конфликте PK строка не вставляется — ок. - 3.2
download.go:510: инлайнupdated_at = datetime('now')→ параметрFormatTime(store.Now())(sortability-ловушка — проверить, что не осталось сырыхdatetime('now')в SQL:grep -rn "datetime('now')" internal).
4. Миграция БД (goose)
- 4.1 Новая миграция (стиль
0006): backfill каждой timestamp-колонкиstrftime('%Y-%m-%dT%H:%M:%SZ', col)(7 таблиц: download.created_at/updated_at/ source_added_at, download_infohash/recognition/hint/override/metadata_candidate/ file_link .created_at) + rebuild таблиц безDEFAULTна timestamp-колонках. - 4.2 Down-миграция: обратный backfill
strftime('%Y-%m-%d %H:%M:%S', col)+ восстановлениеDEFAULT. - 4.3 Тест миграции: строки со старым форматом → новый формат; хронология
сортировки сохранена;
source_added_at IS NULLне затронут; обратный backfill (down) на строке сT/Zдаёт старый формат. - 4.4 Обновить существующий
TestUlidMigration(migration_test.go:100-101): ассерт ждёт старый формат2026-01-01 10:00:00, после новой миграции метки станут...T...Z— поправить ожидаемое (заодно канарейка, что backfill отработал).
5. Настройка таймзоны отображения
- 5.1
internal/config: секция[general](General struct) с полемTimezone string(дефолтUTC) + распарсенный*time.Location; жёсткая валидация на старте черезtime.LoadLocation, невалидная зона → ошибка конфигурации (без рантайм-фолбэка). - 5.1a Встроить zoneinfo:
import _ "time/tzdata"в точке входа (cmd/jellybit). ТогдаLoadLocationпадает только на битом имени. - 5.2
internal/httpapi: пробросить*time.LocationдоfmtDate/humanizeAge(свободные функции — тянуть параметром черезtoView, вызовыhttpapi.go:288,545, либо сделать методамиserver); заменить зашитыйmskLoc(live.go:189-201) целиком (после встраивания tzdata рантайм-фолбэк на UTC внутриmskLocне нужен). Guard:t.In(nil)паникует — при незаданной зоне подставлятьtime.UTC; тест-хелперtestRouterLiveтоже задаёт loc. - 5.3 Обновить пример конфига (
config.example.toml/деплой): секция[general]первой,timezone; для этого сервера —Europe/Moscow.
6. Документация
- 6.1
docs/conventions/database.md:42-43: формат меток → RFC 3339 (UTC,Z), генерация только из Go (store.Now/FormatTime), безDEFAULT. - 6.2
docs/conventions/config.md: зона отображения конфигурируема (дефолт UTC); хранение и логи — UTC. - 6.3
docs/specs/database.md: комментарий формата timestamp-колонок в ER.
7. Проверка и ревью кода (чекпоинт до archive)
- 7.1
task test/task lintзелёные; тест сортировки списка на смешанных и однотипных метках. - 7.2 Ревью кода реализации; затем
openspec validate --strictи archive.