176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
package memory
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// openTemp opens a Store backed by a temp-file DB and registers cleanup.
|
|
func openTemp(t *testing.T) *Store {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "sub", "memory.db") // sub/ must be created by Open
|
|
st, err := Open(dbPath, dir, "")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
return st
|
|
}
|
|
|
|
// tableExists reports whether a table or virtual table with the given name
|
|
// exists in sqlite_master.
|
|
func tableExists(t *testing.T, st *Store, name string) bool {
|
|
t.Helper()
|
|
var got string
|
|
err := st.DB().QueryRow(
|
|
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name,
|
|
).Scan(&got)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return got == name
|
|
}
|
|
|
|
// TestFTS5SmokeTest de-risks the pure-Go SQLite FTS5 dependency: it asserts the
|
|
// FTS5 virtual table is created without error and is queryable.
|
|
func TestFTS5SmokeTest(t *testing.T) {
|
|
st := openTemp(t)
|
|
|
|
if !tableExists(t, st, "memory_fts") {
|
|
t.Fatalf("memory_fts virtual table not created")
|
|
}
|
|
|
|
// A MATCH query must run without error (proves FTS5 is compiled in).
|
|
rows, err := st.DB().Query(`SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?`, "anything")
|
|
if err != nil {
|
|
t.Fatalf("FTS5 MATCH query failed (FTS5 not available?): %v", err)
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
// TestSchemaObjectsCreated verifies the content table, indexes and triggers.
|
|
func TestSchemaObjectsCreated(t *testing.T) {
|
|
st := openTemp(t)
|
|
|
|
if !tableExists(t, st, "memory_index") {
|
|
t.Fatalf("memory_index table not created")
|
|
}
|
|
|
|
for _, idx := range []string{"memory_index_scope_idx", "memory_index_type_idx"} {
|
|
var n int
|
|
if err := st.DB().QueryRow(
|
|
`SELECT count(*) FROM sqlite_master WHERE type='index' AND name=?`, idx,
|
|
).Scan(&n); err != nil || n != 1 {
|
|
t.Fatalf("index %q missing (n=%d, err=%v)", idx, n, err)
|
|
}
|
|
}
|
|
|
|
for _, trg := range []string{"memory_ai", "memory_ad", "memory_au"} {
|
|
var n int
|
|
if err := st.DB().QueryRow(
|
|
`SELECT count(*) FROM sqlite_master WHERE type='trigger' AND name=?`, trg,
|
|
).Scan(&n); err != nil || n != 1 {
|
|
t.Fatalf("trigger %q missing (n=%d, err=%v)", trg, n, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// insertRow inserts a memory_index row and returns its id.
|
|
func insertRow(t *testing.T, st *Store, path, body string) int64 {
|
|
t.Helper()
|
|
res, err := st.DB().Exec(
|
|
`INSERT INTO memory_index (path, scope, scope_id, type, body, fingerprint, last_indexed_at)
|
|
VALUES (?, 'global', '', 'free', ?, 'fp', 0)`, path, body)
|
|
if err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
id, err := res.LastInsertId()
|
|
if err != nil {
|
|
t.Fatalf("last insert id: %v", err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// ftsMatchCount returns how many FTS rows match the given single-term query.
|
|
func ftsMatchCount(t *testing.T, st *Store, term string) int {
|
|
t.Helper()
|
|
var n int
|
|
if err := st.DB().QueryRow(
|
|
`SELECT count(*) FROM memory_fts WHERE memory_fts MATCH ?`, term,
|
|
).Scan(&n); err != nil {
|
|
t.Fatalf("fts match count %q: %v", term, err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
// TestTriggerSyncInsertDeleteUpdate verifies the AFTER INSERT/DELETE/UPDATE
|
|
// triggers keep memory_fts in sync with memory_index.
|
|
func TestTriggerSyncInsertDeleteUpdate(t *testing.T) {
|
|
st := openTemp(t)
|
|
|
|
// INSERT -> searchable.
|
|
id := insertRow(t, st, "/mem/a.md", "alpha bravo charlie")
|
|
if got := ftsMatchCount(t, st, "bravo"); got != 1 {
|
|
t.Fatalf("after insert: MATCH bravo = %d, want 1", got)
|
|
}
|
|
|
|
// UPDATE -> re-synced (old term gone, new term present).
|
|
if _, err := st.DB().Exec(`UPDATE memory_index SET body=? WHERE id=?`, "delta echo", id); err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if got := ftsMatchCount(t, st, "bravo"); got != 0 {
|
|
t.Fatalf("after update: MATCH bravo = %d, want 0", got)
|
|
}
|
|
if got := ftsMatchCount(t, st, "echo"); got != 1 {
|
|
t.Fatalf("after update: MATCH echo = %d, want 1", got)
|
|
}
|
|
|
|
// DELETE -> removed from index.
|
|
if _, err := st.DB().Exec(`DELETE FROM memory_index WHERE id=?`, id); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if got := ftsMatchCount(t, st, "echo"); got != 0 {
|
|
t.Fatalf("after delete: MATCH echo = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
// TestOpenIdempotent verifies that running the migration twice on the same file
|
|
// is safe and preserves data.
|
|
func TestOpenIdempotent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "memory.db")
|
|
|
|
st1, err := Open(dbPath, dir, "")
|
|
if err != nil {
|
|
t.Fatalf("first Open: %v", err)
|
|
}
|
|
insertRow(t, st1, "/mem/keep.md", "persistent needle")
|
|
if err := st1.Close(); err != nil {
|
|
t.Fatalf("close first: %v", err)
|
|
}
|
|
|
|
// Re-open: migration must not error and existing data must survive.
|
|
st2, err := Open(dbPath, dir, "")
|
|
if err != nil {
|
|
t.Fatalf("second Open (migration not idempotent?): %v", err)
|
|
}
|
|
t.Cleanup(func() { st2.Close() })
|
|
|
|
if got := ftsMatchCount(t, st2, "needle"); got != 1 {
|
|
t.Fatalf("after reopen: MATCH needle = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
// TestInMemoryOpen verifies the ":memory:" DSN works (no parent dir creation).
|
|
func TestInMemoryOpen(t *testing.T) {
|
|
st, err := Open(":memory:", "", "")
|
|
if err != nil {
|
|
t.Fatalf("Open in-memory: %v", err)
|
|
}
|
|
defer st.Close()
|
|
if !tableExists(t, st, "memory_fts") {
|
|
t.Fatalf("memory_fts not created in in-memory db")
|
|
}
|
|
}
|