fix(store): 修正邻接查询、GC 竞态、备份权限与设置解析,新增 OpenData 纯数据打开

邻接查询 tie-break 与 pinned 排序键与列表序对齐,补表驱动测试(round2 P1-1); GC DELETE 带条件复查,防 SELECT 与 DELETE 间恢复/引用竞态(P2-6); 备份经 umask 收紧创建即 0600,消除 chmod 前暴露窗口(P2-7); page_size 改 strconv.Atoi 全文解析,拒部分解析脏值(P2-13); OpenData 不迁移、不做版本守卫(D6/P1-5)。
This commit is contained in:
2026-09-08 17:32:47 +08:00
parent a885b52f18
commit 132c4a1ba2
5 changed files with 162 additions and 26 deletions
+92
View File
@@ -97,6 +97,98 @@ func TestForeignKeysCascade(t *testing.T) {
}
}
// TestOpenDataNoMigrate 维护命令打开方式:不迁移、不做版本守卫(D6,评审 round2 P1-5)。
func TestOpenDataNoMigrate(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "pn.db")
// 全新路径:OpenData 不创建 schema
s, err := OpenData(dbPath)
if err != nil {
t.Fatal(err)
}
v, err := s.SchemaVersion()
if err != nil {
t.Fatal(err)
}
if v != 0 {
t.Fatalf("OpenData 不应执行迁移,user_version=%d", v)
}
var one int
if err := s.db.QueryRow("SELECT 1 FROM notes LIMIT 1").Scan(&one); err == nil {
t.Fatal("OpenData 不应创建表")
}
s.Close()
// 更高 schema 版本:OpenData 不被守卫拒绝(升级/回滚场景备份仍可用)
s2, err := Open(dbPath, false)
if err != nil {
t.Fatal(err)
}
if _, err := s2.DB().Exec("PRAGMA user_version = 99"); err != nil {
t.Fatal(err)
}
s2.Close()
if _, err := Open(dbPath, false); !errors.Is(err, ErrSchemaNewer) {
t.Fatalf("Open 仍应被守卫拒绝,实际 %v", err)
}
s3, err := OpenData(dbPath)
if err != nil {
t.Fatalf("OpenData 不做版本守卫,应放行: %v", err)
}
s3.Close()
}
// TestAdjacentPublicNote 邻接查询:列表序 pinned DESC, updated_at DESC, id DESC
// 的紧邻语义(评审 round2 P1-1:同秒 tie-break 与 pinned 参与排序;仅统计公开)。
func TestAdjacentPublicNote(t *testing.T) {
s := openTestStore(t)
mk := func(slug string, at int64, pinned bool, status string) {
t.Helper()
if _, err := s.CreateNote(&Note{Slug: slug, Title: slug, Status: status,
Tags: []string{}, Pinned: pinned, CreatedAt: at, UpdatedAt: at}); err != nil {
t.Fatal(err)
}
}
mk("a", 100, false, "public") // id=1
mk("b", 100, false, "public") // id=2(与 a 同秒,列表中排在 a 之前)
mk("c", 80, false, "public") // id=3
mk("p", 50, true, "public") // id=4 置顶 → 列表首
mk("priv", 200, false, "private") // 更新的私有笔记:不得出现在邻接中
get := func(slug string) *Note {
n, err := s.GetNoteBySlug(slug)
if err != nil {
t.Fatal(err)
}
return n
}
// 公开列表序:p > b > a > c(priv 被过滤)
cases := []struct{ slug, prev, next string }{
{"p", "", "b"}, // 置顶首篇
{"b", "p", "a"},
{"a", "b", "c"}, // a 与 b 同秒:prev 应取同秒中紧邻的 b(id 更大),而非跳过
{"c", "a", ""},
}
for _, tc := range cases {
prevSlug, _, err := s.AdjacentPublicNote(get(tc.slug), "prev")
if err != nil {
t.Fatalf("%s prev: %v", tc.slug, err)
}
nextSlug, _, err := s.AdjacentPublicNote(get(tc.slug), "next")
if err != nil {
t.Fatalf("%s next: %v", tc.slug, err)
}
if prevSlug != tc.prev || nextSlug != tc.next {
t.Errorf("%s: 期望 prev=%q next=%q,实际 prev=%q next=%q",
tc.slug, tc.prev, tc.next, prevSlug, nextSlug)
}
}
if _, _, err := s.AdjacentPublicNote(get("a"), "bad"); err == nil {
t.Error("非法 dir 应返回错误")
}
}
func openTestStore(t *testing.T) *Store {
t.Helper()
s, err := Open(filepath.Join(t.TempDir(), "test.db"), false)