package vector import ( "context" "os" "testing" ) // 本文件锁定向量的**清除侧**(Deleter):到期清扫删行时同步删向量,否则过期 // 向量会占掉 idx.Search 的 top-K 预算(切片 M1 的 reviewer 交上来的 finding)。 // TestSQLite_Delete_正常 覆盖删除的正例与三条边界:删掉的查不到、没删的还在、 // 删不存在的 id 幂等成功、空 ids 空操作。 func TestSQLite_Delete_正常(t *testing.T) { t.Parallel() ctx := context.Background() s := newInMemorySQLite(t, 3) for _, id := range []string{"cat", "dog", "bird"} { if err := s.Add(ctx, id, Vector{1, 0, 0}); err != nil { t.Fatalf("Add(%s): %v", id, err) } } if err := s.Delete(ctx, "cat"); err != nil { t.Fatalf("Delete(cat): %v", err) } hits, err := s.Search(ctx, Vector{1, 0, 0}, 10) if err != nil { t.Fatalf("Search: %v", err) } for _, h := range hits { if h.ID == "cat" { t.Fatal("被删的向量还在索引里") } } if len(hits) != 2 { t.Fatalf("剩余命中 = %d, want 2(只该删掉那一条)", len(hits)) } // 幂等:再删一次同一个 id 不报错(删不存在的行本就是空操作)。 if err := s.Delete(ctx, "cat"); err != nil { t.Fatalf("重复 Delete 应当幂等成功: %v", err) } // 空 ids / 全是空白的 id:空操作,不报错。 if err := s.Delete(ctx); err != nil { t.Fatalf("Delete() 空 ids 应当是无操作: %v", err) } if err := s.Delete(ctx, " "); err != nil { t.Fatalf("Delete(空白 id) 应当是无操作: %v", err) } // 批删:一次给两个 id 走的是同一条占位串路径。 if err := s.Delete(ctx, "dog", "bird"); err != nil { t.Fatalf("批量 Delete: %v", err) } hits, err = s.Search(ctx, Vector{1, 0, 0}, 10) if err != nil { t.Fatalf("Search after batch delete: %v", err) } if len(hits) != 0 { t.Fatalf("批量删后仍有 %d 条命中", len(hits)) } } // TestSQLite_Delete_取消的ctx_异常:ctx 已取消时删除直接报错,绝不静默成功。 func TestSQLite_Delete_取消的ctx_异常(t *testing.T) { t.Parallel() s := newInMemorySQLite(t, 3) ctx, cancel := context.WithCancel(context.Background()) cancel() if err := s.Delete(ctx, "cat"); err == nil { t.Fatal("已取消的 ctx 下 Delete 应当报错") } } // TestVecDeletePlaceholders 钉住占位串与参数表的形状:SQLite 用 `?`、pgvector // 用 `$n`,空白 id 被跳过,全空白时返回空占位串。 func TestVecDeletePlaceholders(t *testing.T) { t.Parallel() tests := []struct { name string ids []string dollar bool wantSQL string wantN int }{ {name: "sqlite 单个", ids: []string{"a"}, wantSQL: "?", wantN: 1}, {name: "sqlite 三个", ids: []string{"a", "b", "c"}, wantSQL: "?,?,?", wantN: 3}, {name: "pg 两个", ids: []string{"a", "b"}, dollar: true, wantSQL: "$1,$2", wantN: 2}, {name: "跳过空白", ids: []string{"a", "", " ", "b"}, dollar: true, wantSQL: "$1,$2", wantN: 2}, {name: "全空", ids: []string{"", " "}, wantSQL: "", wantN: 0}, {name: "无参数", ids: nil, wantSQL: "", wantN: 0}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() sql, args := vecDeletePlaceholders(tc.ids, tc.dollar) if sql != tc.wantSQL { t.Fatalf("占位串 = %q, want %q", sql, tc.wantSQL) } if len(args) != tc.wantN { t.Fatalf("参数数 = %d, want %d", len(args), tc.wantN) } }) } } // TestSQLite_Delete_底层错误_异常:库已关闭时删除如实报错(而不是静默留下一堆 // 永远删不掉的向量)。 func TestSQLite_Delete_底层错误_异常(t *testing.T) { t.Parallel() ctx := context.Background() db := openTestDB(t) s, err := NewSQLite(ctx, db, 3, testLogger()) if err != nil { t.Fatalf("NewSQLite: %v", err) } if err := db.Close(); err != nil { t.Fatalf("close db: %v", err) } if err := s.Delete(ctx, "cat"); err == nil { t.Fatal("库已关闭时 Delete 应当报错") } } // TestPG_Delete_RequiresEnv 在设置了 DB_PG_URL 时跑一次真实的 pgvector 删除往返, // 否则跳过(与 TestPG_RequiresEnv 同一口径:本环境默认没有 PG 服务)。 func TestPG_Delete_RequiresEnv(t *testing.T) { url := os.Getenv("DB_PG_URL") if url == "" { t.Skip("DB_PG_URL not set; skipping real PostgreSQL+pgvector integration test") } ctx := context.Background() idx, err := NewPG(ctx, url, 3, testLogger()) if err != nil { t.Fatalf("NewPG: %v", err) } defer func() { _ = idx.Close() }() if err := idx.Add(ctx, "delete-me", Vector{1, 0, 0}); err != nil { t.Fatalf("Add: %v", err) } if err := idx.Delete(ctx, "delete-me"); err != nil { t.Fatalf("Delete: %v", err) } hits, err := idx.Search(ctx, Vector{1, 0, 0}, 10) if err != nil { t.Fatalf("Search: %v", err) } for _, h := range hits { if h.ID == "delete-me" { t.Fatal("pgvector 上被删的向量还在") } } // 幂等。 if err := idx.Delete(ctx, "delete-me"); err != nil { t.Fatalf("重复 Delete 应当幂等: %v", err) } // 空 ids / 空白 id 空操作。 if err := idx.Delete(ctx); err != nil { t.Fatalf("空 ids: %v", err) } if err := idx.Delete(ctx, ""); err != nil { t.Fatalf("空白 id: %v", err) } // 反例对照:确认上面的 Search 不是因为「整张表空了」才没有命中。 if err := idx.Add(ctx, "keep-me", Vector{1, 0, 0}); err != nil { t.Fatalf("Add keep-me: %v", err) } hits, err = idx.Search(ctx, Vector{1, 0, 0}, 10) if err != nil { t.Fatalf("Search keep-me: %v", err) } found := false for _, h := range hits { if h.ID == "keep-me" { found = true } } if !found { t.Fatal("对照组失败:没删的向量也查不到,说明 Search 本身没工作") } }