2026-04-23 17:46:11 -04:00
|
|
|
//! SQLite schema, on-disk open/recreate, and row-level repository helpers.
|
2026-04-21 23:00:47 -04:00
|
|
|
//!
|
2026-08-02 19:04:30 -04:00
|
|
|
//! Policy: the indexer (owner) opens via [`open::open_or_recreate`], which on
|
2026-08-09 16:25:43 -04:00
|
|
|
//! any schema mismatch wipes the DB and rebuilds from
|
|
|
|
|
//! [`schema::SCHEMA_CURRENT`]; there are no in-place migrations. *Consumers*
|
|
|
|
|
//! (search, status, size, `clear`) use [`open::open_existing`], which never
|
|
|
|
|
//! creates or wipes — a mismatch is an error, not data loss.
|
2026-04-21 23:00:47 -04:00
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
use std::sync::Mutex;
|
|
|
|
|
|
|
|
|
|
use rusqlite::{Connection, InterruptHandle};
|
|
|
|
|
|
2026-08-02 20:21:19 -04:00
|
|
|
pub mod key;
|
2026-04-23 17:46:11 -04:00
|
|
|
pub mod open;
|
2026-04-21 23:00:47 -04:00
|
|
|
pub mod repo;
|
|
|
|
|
pub mod schema;
|
|
|
|
|
|
2026-08-02 20:21:19 -04:00
|
|
|
pub use key::{process_key_hex, set_process_key};
|
|
|
|
|
pub use open::{
|
2026-08-20 02:34:08 -04:00
|
|
|
index_needs_rebuild, key_mismatch_parts, open_existing, open_or_recreate, verify_process_key,
|
|
|
|
|
KeyMismatch, CURRENT_SCHEMA_VERSION, KEY_MISMATCH_PREFIX,
|
2026-08-02 20:21:19 -04:00
|
|
|
};
|
2026-08-05 18:05:04 -04:00
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Bumped whenever the index file is replaced rather than modified — a
|
|
|
|
|
/// rebuild and a clear both put a *new* file at the *same* path, so a
|
|
|
|
|
/// long-lived connection that misses the swap keeps serving the deleted
|
|
|
|
|
/// inode. [`crate::search`] compares this against the value it opened with.
|
2026-08-05 19:17:11 -04:00
|
|
|
static INDEX_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
|
|
|
|
|
|
|
|
/// The current index generation. See [`INDEX_EPOCH`].
|
|
|
|
|
pub fn index_epoch() -> u64 {
|
|
|
|
|
INDEX_EPOCH.load(std::sync::atomic::Ordering::SeqCst)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Declare that the index file has been replaced.
|
|
|
|
|
pub fn bump_index_epoch() {
|
|
|
|
|
INDEX_EPOCH.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// A shared slot holding the interrupt handle of whatever long statement is
|
|
|
|
|
/// running, so another thread can cut it short.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Protocol: SQLite's interrupt is the only way out of a statement already in
|
|
|
|
|
/// flight — a stop flag can only prevent the *next* one from starting — so
|
|
|
|
|
/// every cancellation is the pair (flag, interrupt). An interrupted statement
|
|
|
|
|
/// fails indistinguishably from a real error, so callers re-read the flag on
|
|
|
|
|
/// the error path to tell cancellation from breakage.
|
2026-08-05 18:05:04 -04:00
|
|
|
pub type InterruptSlot = Mutex<Option<InterruptHandle>>;
|
|
|
|
|
|
|
|
|
|
/// Publish `conn`'s interrupt handle in `slot` for as long as this lives.
|
|
|
|
|
pub struct InterruptGuard<'a> {
|
|
|
|
|
slot: &'a InterruptSlot,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> InterruptGuard<'a> {
|
|
|
|
|
pub fn arm(slot: &'a InterruptSlot, conn: &Connection) -> InterruptGuard<'a> {
|
2026-08-09 16:25:43 -04:00
|
|
|
*crate::lock_ok(slot) = Some(conn.get_interrupt_handle());
|
2026-08-05 18:05:04 -04:00
|
|
|
InterruptGuard { slot }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for InterruptGuard<'_> {
|
|
|
|
|
fn drop(&mut self) {
|
2026-08-09 16:25:43 -04:00
|
|
|
*crate::lock_ok(self.slot) = None;
|
2026-08-05 18:05:04 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Interrupt the statement `slot` holds a handle for, if there is one.
|
|
|
|
|
/// A no-op otherwise, and on a connection that has since closed.
|
|
|
|
|
pub fn interrupt(slot: &InterruptSlot) {
|
2026-08-09 16:25:43 -04:00
|
|
|
if let Some(handle) = crate::lock_ok(slot).as_ref() {
|
|
|
|
|
handle.interrupt();
|
2026-08-05 18:05:04 -04:00
|
|
|
}
|
|
|
|
|
}
|