'use strict'; // Full-text search index in /.notes/index.sqlite (SQLite FTS5). // The Markdown files are the source of truth; this file can be deleted and is rebuilt on launch. // Uses better-sqlite3 in the app; falls back to node:sqlite so the tests run on plain Node 22. function loadDriver() { try { return require('better-sqlite3'); } catch { const { DatabaseSync } = require('node:sqlite'); return DatabaseSync; } } const likeEscape = (s) => s.replace(/[\\%_]/g, (c) => '\\' + c); function openDb(file) { const Database = loadDriver(); const db = new Database(file); db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS notes USING fts5( id UNINDEXED, title, tags, body, tokenize = 'unicode61 remove_diacritics 2' )`); const del = db.prepare('DELETE FROM notes WHERE id = ?'); const delTree = db.prepare("DELETE FROM notes WHERE id = ? OR id LIKE ? ESCAPE '\\'"); const ins = db.prepare('INSERT INTO notes (id, title, tags, body) VALUES (?, ?, ?, ?)'); const find = db.prepare(`SELECT id, title, snippet(notes, 3, char(1), char(2), '…', 14) AS snip FROM notes WHERE notes MATCH ? ORDER BY bm25(notes, 0.0, 8.0, 4.0, 1.0) LIMIT ?`); const row = (p) => [p.id, p.title || '', (p.tags || []).join(' '), p.body || '']; return { upsert(p) { del.run(p.id); ins.run(...row(p)); }, remove(id) { delTree.run(id, likeEscape(id) + '/%'); }, rebuild(pages) { db.exec('BEGIN'); try { db.exec('DELETE FROM notes'); for (const p of pages) if (p.exists) ins.run(...row(p)); db.exec('COMMIT'); } catch (err) { db.exec('ROLLBACK'); throw err; } }, search(q, limit = 20) { const parts = String(q).match(/(?:tag:|in:|)\S+/g) || []; const terms = []; const fields = []; for (const p of parts) { if (p.startsWith('tag:')) { fields.push('tags: ' + p.slice(4).replace(/[^\w-]/g, '')); } else if (p.startsWith('in:')) { fields.push('title: ' + p.slice(3).replace(/[^\w-]/g, '')); } else { terms.push(p); } } const words = (terms.join(' ').match(/[\p{L}\p{N}_]+/gu) || []).slice(0, 8); let match = words.map((t) => `"${t}"*`).join(' '); if (fields.length) { match = fields.map(f => `${f.split(':')[0]}: "${f.split(':')[1].trim()}"*`).join(' ') + (match ? ' ' + match : ''); } if (!match) return []; try { return find.all(match, limit).map((r) => ({ id: r.id, title: r.title, snip: r.snip })); } catch { return []; } }, close() { try { db.close(); } catch { /* already closed */ } } }; } module.exports = { openDb };