Хранение времени: 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>
This commit is contained in:
@@ -71,11 +71,12 @@ func (s *Store) CreateRecognition(ctx context.Context, r *Recognition, reasons [
|
||||
const q = `
|
||||
INSERT INTO recognition
|
||||
(id, download_id, attempt_no, is_current, media_type, title, original_title,
|
||||
year, provider, provider_id, confidence, reasons, raw_llm, plan)
|
||||
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
year, provider, provider_id, confidence, reasons, raw_llm, plan, created_at)
|
||||
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
if _, err := tx.ExecContext(ctx, q,
|
||||
r.ID, r.DownloadID, nextAttempt, r.MediaType, r.Title, r.OriginalTitle,
|
||||
r.Year, r.Provider, r.ProviderID, r.Confidence, string(reasonsJSON), r.RawLLM, r.Plan); err != nil {
|
||||
r.Year, r.Provider, r.ProviderID, r.Confidence, string(reasonsJSON), r.RawLLM, r.Plan,
|
||||
FormatTime(Now())); err != nil {
|
||||
return "", fmt.Errorf("insert recognition: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -105,8 +106,8 @@ func (s *Store) GetCurrentRecognition(ctx context.Context, downloadID string) (*
|
||||
// AddHint добавляет текстовую подсказку ревьюера к загрузке.
|
||||
func (s *Store) AddHint(ctx context.Context, downloadID string, text string) error {
|
||||
if _, err := s.DB.ExecContext(ctx,
|
||||
`INSERT INTO hint (id, download_id, text) VALUES (?, ?, ?)`,
|
||||
ident.NewID(), downloadID, text); err != nil {
|
||||
`INSERT INTO hint (id, download_id, text, created_at) VALUES (?, ?, ?, ?)`,
|
||||
ident.NewID(), downloadID, text, FormatTime(Now())); err != nil {
|
||||
return fmt.Errorf("add hint: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -127,9 +128,9 @@ func (s *Store) ListHints(ctx context.Context, downloadID string) ([]string, err
|
||||
// SetOverride пиннит значение поля (upsert по (download_id, field)).
|
||||
func (s *Store) SetOverride(ctx context.Context, downloadID string, field, value string) error {
|
||||
const q = `
|
||||
INSERT INTO override (id, download_id, field, value) VALUES (?, ?, ?, ?)
|
||||
INSERT INTO override (id, download_id, field, value, created_at) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (download_id, field) DO UPDATE SET value = excluded.value`
|
||||
if _, err := s.DB.ExecContext(ctx, q, ident.NewID(), downloadID, field, value); err != nil {
|
||||
if _, err := s.DB.ExecContext(ctx, q, ident.NewID(), downloadID, field, value, FormatTime(Now())); err != nil {
|
||||
return fmt.Errorf("set override %q: %w", field, err)
|
||||
}
|
||||
return nil
|
||||
@@ -181,12 +182,13 @@ func (s *Store) CreateFileLinks(ctx context.Context, links []FileLink) error {
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
now := FormatTime(Now())
|
||||
const q = `
|
||||
INSERT INTO file_link (id, download_id, apply_batch_id, src_path, dst_path, kind, status, size)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
INSERT INTO file_link (id, download_id, apply_batch_id, src_path, dst_path, kind, status, size, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for _, l := range links {
|
||||
if _, err := tx.ExecContext(ctx, q,
|
||||
ident.NewID(), l.DownloadID, l.ApplyBatchID, l.SrcPath, l.DstPath, l.Kind, l.Status, l.Size); err != nil {
|
||||
ident.NewID(), l.DownloadID, l.ApplyBatchID, l.SrcPath, l.DstPath, l.Kind, l.Status, l.Size, now); err != nil {
|
||||
return fmt.Errorf("insert file_link: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -328,12 +330,13 @@ func (s *Store) CreateCandidates(ctx context.Context, cands []MetadataCandidate)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
now := FormatTime(Now())
|
||||
const q = `
|
||||
INSERT INTO metadata_candidate (id, recognition_id, provider, provider_id, title, year, url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
INSERT INTO metadata_candidate (id, recognition_id, provider, provider_id, title, year, url, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for _, c := range cands {
|
||||
if _, err := tx.ExecContext(ctx, q,
|
||||
ident.NewID(), c.RecognitionID, c.Provider, c.ProviderID, c.Title, c.Year, c.URL); err != nil {
|
||||
ident.NewID(), c.RecognitionID, c.Provider, c.ProviderID, c.Title, c.Year, c.URL, now); err != nil {
|
||||
return fmt.Errorf("insert candidate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user