Commit84d8a3dcRecorded24 Oct 2026Repositoryfolio

Cover metadata-only search and literal FTS replacement compatibility

Changed
 test/test-fts-replacement.sgl   | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 test/test-search-projection.sgl | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 95 insertions(+)
Diff
test/test-fts-replacement.sgladded
@@ -0,0 +1,47 @@
+1
;;; FTS replacement must remove every literal-name row, including legacy rows.
+2
(import (sigil test)
+3
(sigil fs)
+4
(sigil path)
+5
(sigil sqlite)
+6
(folio store)
+7
(folio index))
+8
+9
(test-group "literal FTS replacement"
+10
(test "updates and removal preserve unrelated names, arbitrary rowids and tokenless names"
+11
(call-with-temp-directory
+12
(lambda (root)
+13
(let* ((store (make-folio-store root))
+14
(idx (make-folio-index store))
+15
(db (folio-index-db idx)))
+16
(for-each
+17
(lambda (name)
+18
(let* ((path (path-join (store-notes-dir store)
+19
(string-append name ".md")))
+20
(sibling (string-append "guard-" name)))
+21
(write-file-string path "# Before\nbeforeword\n")
+22
(index-note! idx path)
+23
;; Standalone FTS rowids are unrelated to notes_meta rowids.
+24
;; Old/concurrent versions can leave more than one FTS row.
+25
(assert-true
+26
(sqlite-run db
+27
"INSERT INTO notes_fts (name, title, tags, content) VALUES (?, ?, '', 'staleword')"
+28
name name))
+29
(assert-true
+30
(sqlite-run db
+31
"INSERT INTO notes_fts (name, title, tags, content) VALUES (?, ?, '', 'guardword')"
+32
sibling name))
+33
(write-file-string path "# After\nafterword\n")
+34
(index-note! idx path)
+35
(let ((rows (sqlite-query db
+36
"SELECT content FROM notes_fts WHERE name = ?" name)))
+37
(assert-equal 1 (length rows))
+38
(assert-equal "# After\nafterword\n" (cdr (assoc 'content (car rows)))))
+39
(assert-equal 1
+40
(length (sqlite-query db "SELECT rowid FROM notes_fts WHERE name = ?" sibling)))
+41
(remove-note-from-index! idx name)
+42
(assert-equal '()
+43
(sqlite-query db "SELECT rowid FROM notes_fts WHERE name = ?" name))
+44
(assert-equal 1
+45
(length (sqlite-query db "SELECT rowid FROM notes_fts WHERE name = ?" sibling)))))
+46
'("normal" "a\"quoted" "中文" "---" "😀" "Case-Fold" "a.b" "with space"))
+47
(sqlite-close db))))))
test/test-search-projection.sgladded
@@ -0,0 +1,48 @@
+1
;;; Metadata-only searches retain ranking and filters without computing snippets.
+2
(import (sigil test)
+3
(sigil fs)
+4
(sigil path)
+5
(sigil sqlite)
+6
(folio store)
+7
(folio index))
+8
+9
(define (without-snippets rows)
+10
(map (lambda (row)
+11
(filter (lambda (field) (not (eq? (car field) 'snippet))) row))
+12
rows))
+13
+14
(test-group "search projections"
+15
(test "the default retains snippets; opting out preserves ranked metadata and filters"
+16
(call-with-temp-directory
+17
(lambda (root)
+18
(let* ((store (make-folio-store root))
+19
(dir (store-notes-dir store)))
+20
(write-file-string (path-join dir "one.md")
+21
"---\ntags: [alpha]\ncreated: 2025-01-01\nlast-updated: 2026-10-24\n---\n# One\nneedle needle needle λ\n")
+22
(write-file-string (path-join dir "two.md")
+23
"---\ntags: [beta]\ncreated: 2024-01-01\n---\n# Two\nneedle λ\n")
+24
(let* ((idx (make-folio-index store))
+25
(db (folio-index-db idx)))
+26
(index-reconcile! idx)
+27
(let ((ordinary (search-index idx "needle" #f #f)))
+28
(assert-equal 2 (length ordinary))
+29
(assert-true
+30
(every (lambda (row) (string? (cdr (assoc 'snippet row)))) ordinary))
+31
(assert-equal (without-snippets ordinary)
+32
(search-index idx "needle" #f #f snippets?: #f)))
+33
(for-each
+34
(lambda (options)
+35
(let* ((query (car options))
+36
(tag (cadr options))
+37
(since (caddr options))
+38
(ordinary (search-index idx query tag since)))
+39
(assert-equal 1 (length ordinary))
+40
(assert-equal (without-snippets ordinary)
+41
(search-index idx query tag since snippets?: #f))))
+42
'(("needle" "alpha" #f)
+43
("needle" #f "2026-01-01")
+44
("λ" "beta" #f)))
+45
(assert-equal (search-index idx #f #f #f)
+46
(search-index idx #f #f #f snippets?: #f))
+47
(assert-equal '() (search-index idx "missingword" #f #f snippets?: #f))
+48
(sqlite-close db)))))))