2026-04-21 23:00:47 -04:00
|
|
|
//! SQL strings for the current schema. Versioned; [`migrate`](super::migrate)
|
|
|
|
|
//! drives the upgrade path.
|
|
|
|
|
|
|
|
|
|
/// Pragmas applied on every connection open. Tuned for write throughput during
|
|
|
|
|
/// indexing; a clean shutdown re-enables journal_mode/synchronous via
|
|
|
|
|
/// [`super::repo::checkpoint_and_close`].
|
|
|
|
|
pub const PRAGMAS_FAST: &str = "
|
|
|
|
|
PRAGMA journal_mode = OFF;
|
|
|
|
|
PRAGMA synchronous = 0;
|
|
|
|
|
PRAGMA cache_size = 10000;
|
|
|
|
|
PRAGMA temp_store = MEMORY;
|
|
|
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
|
";
|
|
|
|
|
|
2026-04-23 17:46:11 -04:00
|
|
|
/// The full current schema. Applied by [`super::open::open_or_recreate`]
|
|
|
|
|
/// when the DB is fresh or has just been wiped because it drifted from
|
|
|
|
|
/// [`super::open::CURRENT_SCHEMA_VERSION`].
|
2026-04-21 23:00:47 -04:00
|
|
|
///
|
2026-04-23 17:46:11 -04:00
|
|
|
/// FTS5 is *contentless* (see [`fts_create_sql`]): the inverted index is kept
|
|
|
|
|
/// but the column values aren't stored. The canonical extracted text lives
|
|
|
|
|
/// in a separate `documents_text` table, zstd-compressed. Snippet rendering
|
|
|
|
|
/// for search results decompresses on demand and highlights matches in Rust
|
|
|
|
|
/// (see `crate::snippet`). This keeps the on-disk footprint close to Baloo's
|
|
|
|
|
/// LMDB-only size while still supporting snippet/highlight features.
|
2026-04-21 23:00:47 -04:00
|
|
|
pub const SCHEMA_CURRENT: &str = r#"
|
|
|
|
|
CREATE TABLE schema_info (
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
value TEXT NOT NULL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE files (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
path TEXT NOT NULL UNIQUE,
|
|
|
|
|
parent TEXT NOT NULL,
|
|
|
|
|
size INTEGER NOT NULL,
|
|
|
|
|
mtime INTEGER NOT NULL,
|
|
|
|
|
inode INTEGER,
|
|
|
|
|
device_id INTEGER,
|
|
|
|
|
mime TEXT,
|
|
|
|
|
type INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
basic_state INTEGER NOT NULL DEFAULT 0, -- 0=pending 1=done 2=failed
|
|
|
|
|
content_state INTEGER NOT NULL DEFAULT 0, -- 0=pending 1=done 2=failed 3=n/a
|
|
|
|
|
failure_msg TEXT,
|
|
|
|
|
hash BLOB
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX idx_files_parent ON files(parent);
|
|
|
|
|
CREATE INDEX idx_files_mtime ON files(mtime);
|
|
|
|
|
CREATE INDEX idx_files_type ON files(type);
|
|
|
|
|
CREATE INDEX idx_files_mime ON files(mime);
|
|
|
|
|
CREATE INDEX idx_files_hash ON files(hash);
|
|
|
|
|
CREATE INDEX idx_files_content_pending ON files(id) WHERE content_state = 0;
|
|
|
|
|
|
|
|
|
|
CREATE TABLE properties (
|
|
|
|
|
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
|
|
|
key TEXT NOT NULL,
|
|
|
|
|
value TEXT NOT NULL,
|
|
|
|
|
PRIMARY KEY (file_id, key)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE failed_files (
|
|
|
|
|
file_id INTEGER PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
|
|
|
|
|
reason TEXT,
|
|
|
|
|
ts INTEGER NOT NULL
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-23 17:46:11 -04:00
|
|
|
-- Canonical extracted text for every successfully content-indexed file.
|
|
|
|
|
-- Compressed with zstd (see `crate::db::repo::set_content_done`). Only
|
|
|
|
|
-- written when the extractor produced text; absent rows mean "no body
|
|
|
|
|
-- text" (e.g. an image with only EXIF properties).
|
|
|
|
|
CREATE TABLE documents_text (
|
|
|
|
|
file_id INTEGER PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
|
|
|
|
|
text_zstd BLOB NOT NULL,
|
|
|
|
|
text_len INTEGER NOT NULL -- original byte length pre-compression
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-21 23:00:47 -04:00
|
|
|
CREATE TABLE config_validation (
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
value TEXT NOT NULL
|
|
|
|
|
);
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
/// FTS5 virtual table DDL. Separate because the tokenizer is config-driven.
|
|
|
|
|
///
|
2026-04-23 17:46:11 -04:00
|
|
|
/// *Contentless* FTS5 (`content=''`): the inverted index is built from the
|
|
|
|
|
/// column values supplied on INSERT, but those values are not stored. This
|
|
|
|
|
/// is the main lever that pulls our on-disk footprint down toward Baloo's.
|
|
|
|
|
/// `contentless_delete=1` (SQLite 3.43+) lets us `DELETE FROM … WHERE
|
|
|
|
|
/// rowid=?` without replaying the original row text, at the cost of a
|
|
|
|
|
/// modest tombstone bitmap. The tokenizer is config-driven; by default
|
|
|
|
|
/// (`trigram`) we append `remove_diacritics 1` so queries and stored text
|
|
|
|
|
/// fold the same way.
|
|
|
|
|
///
|
|
|
|
|
/// Snippet/highlight aren't available through SQLite's built-in `snippet()`
|
|
|
|
|
/// in contentless mode — we render them in Rust from the zstd-compressed
|
|
|
|
|
/// `documents_text` sidecar instead.
|
2026-04-21 23:00:47 -04:00
|
|
|
pub fn fts_create_sql(tokenizer: &str) -> String {
|
2026-04-23 02:27:41 -04:00
|
|
|
let effective = effective_tokenizer(tokenizer);
|
2026-04-21 23:00:47 -04:00
|
|
|
format!(
|
|
|
|
|
"CREATE VIRTUAL TABLE searchabletext USING fts5(\
|
|
|
|
|
name, text, properties, \
|
2026-04-23 17:46:11 -04:00
|
|
|
tokenize='{}', \
|
|
|
|
|
content='', \
|
|
|
|
|
contentless_delete=1\
|
2026-04-21 23:00:47 -04:00
|
|
|
);",
|
2026-04-23 02:27:41 -04:00
|
|
|
effective.replace('\'', "''")
|
2026-04-21 23:00:47 -04:00
|
|
|
)
|
|
|
|
|
}
|
2026-04-23 02:27:41 -04:00
|
|
|
|
|
|
|
|
/// Map a user-facing tokenizer name to the actual FTS5 option string we
|
|
|
|
|
/// apply. The default `trigram` gets `remove_diacritics 1` appended so an
|
|
|
|
|
/// ASCII query like `cafe` matches indexed `café`, and vice versa.
|
|
|
|
|
/// Without this, the default trigram tokenizer would emit disjoint
|
|
|
|
|
/// trigram sets for the two spellings and `MATCH` would miss one of them.
|
|
|
|
|
/// Users who want precise match semantics can pass the full option string
|
|
|
|
|
/// (e.g. `"trigram case_sensitive 1 remove_diacritics 0"`) and we'll use
|
|
|
|
|
/// it verbatim.
|
|
|
|
|
pub fn effective_tokenizer(tokenizer: &str) -> String {
|
|
|
|
|
let trimmed = tokenizer.trim();
|
|
|
|
|
if trimmed.eq_ignore_ascii_case("trigram") {
|
|
|
|
|
// Explicit default includes accent stripping. `case_sensitive 0`
|
|
|
|
|
// is FTS5's default too; we repeat it here for clarity.
|
|
|
|
|
"trigram remove_diacritics 1".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
trimmed.to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn plain_trigram_gets_accent_stripping() {
|
|
|
|
|
assert_eq!(effective_tokenizer("trigram"), "trigram remove_diacritics 1");
|
|
|
|
|
assert_eq!(effective_tokenizer(" trigram "), "trigram remove_diacritics 1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn explicit_tokenizers_pass_through() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
effective_tokenizer("trigram remove_diacritics 0"),
|
|
|
|
|
"trigram remove_diacritics 0"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(effective_tokenizer("porter"), "porter");
|
|
|
|
|
assert_eq!(effective_tokenizer("unicode61"), "unicode61");
|
|
|
|
|
}
|
|
|
|
|
}
|