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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user