fix(memory): 定宽时间戳存储 + rowid 确定性排序 + 遗留数据归一化

- formatTime 改用固定 30 字节布局(9 位小数秒),让 TEXT 列的字典序与时间序一致(RFC3339Nano 会去掉尾随零产生变宽字符串,秒内会反序)
- memory_items / project / project_chapter 的 ORDER BY 全部补 rowid 兜底,消除同 tick 打平时的顺序不确定
- 开库时幂等归一化变宽的历史值;无法解析的行原样保留,不阻塞开库
- 回归测试:变宽排序反例、同一时间戳插入序、归一化幂等与跳过
This commit is contained in:
AskaEth committed 2026-10-03 16:08:50 +08:00
1 parent 1ac126fc9e
commit 31086ed1ab
10 files changed
+745 -61

No files matched your search

+11 -5
View File
@@ -79,7 +79,8 @@ type rowScanner interface {
// internal/db). A nil db returns ErrNilDB. log may be nil, in which case a
// discard logger is used; the active logger is tagged with source
// "memory/projectindex" so every record is traceable. ctx controls schema
// creation and must be honored by the caller.
// creation and must be honored by the caller. Legacy variable-width timestamps
// are normalized in place (see normalizeTimestamps).
func New(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
if db == nil {
return nil, ErrNilDB
@@ -94,6 +95,9 @@ func New(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
return nil, fmt.Errorf("projectindex: apply schema: %w", err)
}
}
if err := normalizeTimestamps(ctx, db); err != nil {
return nil, err
}
return &Store{db: db, log: log}, nil
}
@@ -106,12 +110,14 @@ func discardLogger(log *slog.Logger) *slog.Logger {
}
// ListProjects returns every project index entry in insertion order (created_at
// ASC, id ASC). An empty (non-nil) slice is returned when there are none, so
// callers can range over it unconditionally. A read error is wrapped with %w so
// callers can use errors.Is against the package sentinels.
// ASC, rowid ASC) — rowid breaks created_at ties so projects written within the
// same clock tick keep their insertion order. An empty (non-nil) slice is
// returned when there are none, so callers can range over it unconditionally. A
// read error is wrapped with %w so callers can use errors.Is against the package
// sentinels.
func (s *Store) ListProjects(ctx context.Context) ([]Project, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, title, desc, created_at, updated_at
FROM project ORDER BY created_at ASC, id ASC`)
FROM project ORDER BY created_at ASC, rowid ASC`)
if err != nil {
return nil, fmt.Errorf("projectindex: list projects: %w", err)
}