fix(memory): 定宽时间戳存储 + rowid 确定性排序 + 遗留数据归一化
- formatTime 改用固定 30 字节布局(9 位小数秒),让 TEXT 列的字典序与时间序一致(RFC3339Nano 会去掉尾随零产生变宽字符串,秒内会反序) - memory_items / project / project_chapter 的 ORDER BY 全部补 rowid 兜底,消除同 tick 打平时的顺序不确定 - 开库时幂等归一化变宽的历史值;无法解析的行原样保留,不阻塞开库 - 回归测试:变宽排序反例、同一时间戳插入序、归一化幂等与跳过
This commit is contained in:
1 parent
1ac126fc9e
commit
31086ed1ab
10 files changed
+745
-61
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewStore_NilDB_异常(t *testing.T) {
|
||||
@@ -210,6 +211,96 @@ func TestStore_List_正常(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStore_List_变宽时间戳_排序 is the regression for the variable-width
|
||||
// timestamp bug: time.RFC3339Nano drops trailing fractional zeros, so the older
|
||||
// item was once stored as ":05.83" and the newer as ":05.8309789", and the text
|
||||
// comparison SQLite applies to these columns ranked the older one as newer.
|
||||
func TestStore_List_变宽时间戳_排序(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
sec := time.Date(2026, 9, 7, 3, 20, 5, 0, time.UTC)
|
||||
older := sec.Add(830 * time.Millisecond) // ":05.83" under RFC3339Nano
|
||||
newer := older.Add(978900 * time.Nanosecond) // ":05.8309789" under RFC3339Nano
|
||||
|
||||
// The newer item is inserted first, so a rowid-only tiebreak would surface
|
||||
// the older one: only correctly ordered created_at text yields 较晚 first.
|
||||
for _, in := range []struct {
|
||||
content string
|
||||
at time.Time
|
||||
}{{"较晚", newer}, {"较早", older}} {
|
||||
item := MemoryItem{PersonaID: "persona.a", Content: in.content, CreatedAt: in.at, UpdatedAt: in.at}
|
||||
if _, err := s.Add(ctx, item); err != nil {
|
||||
t.Fatalf("Add(%s): %v", in.content, err)
|
||||
}
|
||||
}
|
||||
|
||||
var raw string
|
||||
if err := s.db.QueryRowContext(ctx,
|
||||
`SELECT created_at FROM memory_items WHERE content = ?`, "较早").Scan(&raw); err != nil {
|
||||
t.Fatalf("read stored created_at: %v", err)
|
||||
}
|
||||
if len(raw) != storedTimeLen {
|
||||
t.Errorf("stored created_at = %q (%d bytes), want fixed width %d", raw, len(raw), storedTimeLen)
|
||||
}
|
||||
|
||||
got, err := s.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("List = %d items, want 2", len(got))
|
||||
}
|
||||
if got[0].Content != "较晚" {
|
||||
t.Errorf("got[0].Content = %q, want the later timestamp (较晚)", got[0].Content)
|
||||
}
|
||||
if got[1].Content != "较早" {
|
||||
t.Errorf("got[1].Content = %q, want the earlier timestamp (较早)", got[1].Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStore_List_同一时间戳_插入序 covers the tie case: the host clock is coarse
|
||||
// enough that three consecutive Adds can share one timestamp, so ordering must
|
||||
// fall back to rowid rather than an arbitrary row order.
|
||||
func TestStore_List_同一时间戳_插入序(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
at := time.Date(2026, 9, 7, 3, 20, 5, 830978900, time.UTC)
|
||||
for _, content := range []string{"第一条", "第二条", "第三条"} {
|
||||
item := MemoryItem{PersonaID: "persona.a", Content: content, CreatedAt: at, UpdatedAt: at}
|
||||
if _, err := s.Add(ctx, item); err != nil {
|
||||
t.Fatalf("Add(%s): %v", content, err)
|
||||
}
|
||||
}
|
||||
|
||||
want := []string{"第三条", "第二条", "第一条"}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
list func(context.Context) ([]MemoryItem, error)
|
||||
}{
|
||||
{"List", s.List},
|
||||
{"ListByPersona", func(ctx context.Context) ([]MemoryItem, error) {
|
||||
return s.ListByPersona(ctx, "persona.a")
|
||||
}},
|
||||
} {
|
||||
got, err := tc.list(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tc.name, err)
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("%s = %d items, want %d", tc.name, len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if got[i].Content != w {
|
||||
t.Errorf("%s got[%d].Content = %q, want %q (rowid DESC)", tc.name, i, got[i].Content, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Delete_正常(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in new issue
Block a user