#![forbid(unsafe_code)]
pub mod derived;
pub mod error;
pub mod ids;
pub mod observe;
pub mod order_key;
pub mod read;
pub mod schema;
pub mod time;
pub mod tree;
pub mod writers;
use std::path::Path;
use rusqlite::functions::{Context, FunctionFlags};
use rusqlite::types::ValueRef;
use rusqlite::{Connection, Transaction, params};
pub use derived::{GcResult, RebuildTarget};
pub use error::{Error, Result};
pub use ids::{IdMinter, RandomMinter, SequentialMinter, is_valid_id, prefix_of};
pub use observe::{BatchItem, BatchOutcome, DeleteOutcome, ObserveOutcome, has_conflict_markers};
pub use omgbase_reconcile::{Config, MatchBlock, PoolEntry};
pub use read::RevisionRead;
pub use schema::{SCHEMA_SQL, SCHEMA_VERSION};
pub use tree::{TreeEntry, canonical_attrs, canonical_json, serialize_tree_entries, tree_hash};
pub use writers::{NewCommit, NewRevision, Origin, TreeInputBlock};
pub const SPEC_VERSION: &str = "13.0";
pub const FORMAT_MARKDOWN: &str = "markdown";
pub struct Store {
conn: Connection,
minter: Box<dyn IdMinter>,
}
impl std::fmt::Debug for Store {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Store").finish_non_exhaustive()
}
}
#[must_use]
pub fn cosine_bytes(a: &[u8], b: &[u8]) -> f64 {
let floats = |bytes: &[u8]| {
bytes
.chunks_exact(4)
.map(|c| f64::from(f32::from_le_bytes([c[0], c[1], c[2], c[3]])))
.collect::<Vec<f64>>()
};
let (a, b) = (floats(a), floats(b));
let (mut dot, mut na, mut nb) = (0.0, 0.0, 0.0);
for (x, y) in a.iter().zip(&b) {
dot += x * y;
na += x * x;
nb += y * y;
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
fn cosine_udf(ctx: &Context<'_>) -> rusqlite::Result<Option<f64>> {
let blob = |i: usize| -> rusqlite::Result<Option<&[u8]>> {
match ctx.get_raw(i) {
ValueRef::Null => Ok(None),
ValueRef::Blob(b) => Ok(Some(b)),
other => Err(rusqlite::Error::InvalidFunctionParameterType(
i,
other.data_type(),
)),
}
};
match (blob(0)?, blob(1)?) {
(Some(a), Some(b)) => Ok(Some(cosine_bytes(a, b))),
_ => Ok(None),
}
}
impl Store {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
Self::open_with_minter(path, Box::new(RandomMinter))
}
pub fn open_with_minter(path: impl AsRef<Path>, minter: Box<dyn IdMinter>) -> Result<Self> {
let path = path.as_ref();
if path.as_os_str() != ":memory:" {
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
std::fs::create_dir_all(dir)
.map_err(|e| Error::Other(format!("cannot create {}: {e}", dir.display())))?;
}
}
Self::from_connection(Connection::open(path)?, minter)
}
pub fn open_in_memory() -> Result<Self> {
Self::from_connection(Connection::open_in_memory()?, Box::new(RandomMinter))
}
pub fn open_in_memory_with_minter(minter: Box<dyn IdMinter>) -> Result<Self> {
Self::from_connection(Connection::open_in_memory()?, minter)
}
pub fn from_connection(conn: Connection, mut minter: Box<dyn IdMinter>) -> Result<Self> {
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.create_scalar_function(
"cosine",
2,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
cosine_udf,
)?;
schema::migrate(&conn, &mut *minter)?;
Ok(Self { conn, minter })
}
#[must_use]
pub fn conn(&self) -> &Connection {
&self.conn
}
pub fn minter_mut(&mut self) -> &mut dyn IdMinter {
&mut *self.minter
}
pub fn mint(&mut self, prefix: &str) -> String {
self.minter.mint(prefix)
}
pub fn transaction(&self) -> Result<Transaction<'_>> {
Ok(self.conn.unchecked_transaction()?)
}
pub fn create_repo(&mut self, slug: &str) -> Result<String> {
let repo_id = self.minter.mint("rp");
self.conn.execute(
"INSERT INTO repos (repo_id, slug) VALUES (?1, ?2)",
params![repo_id, slug],
)?;
Ok(repo_id)
}
pub fn repo_by_slug(&self, slug: &str) -> Result<Option<String>> {
use rusqlite::OptionalExtension;
Ok(self
.conn
.query_row(
"SELECT repo_id FROM repos WHERE slug = ?1",
params![slug],
|r| r.get(0),
)
.optional()?)
}
pub fn user_version(&self) -> Result<i64> {
schema::user_version(&self.conn)
}
pub fn put_blob(&self, text: &str) -> Result<String> {
writers::put_blob(&self.conn, text)
}
pub fn put_tree_node(&self, entries: &[TreeEntry]) -> Result<String> {
writers::put_tree_node(&self.conn, entries)
}
pub fn write_block_tree(&self, blocks: &[TreeInputBlock]) -> Result<String> {
writers::write_block_tree(&self.conn, blocks)
}
pub fn new_commit(&mut self, input: &NewCommit<'_>) -> Result<(String, i64)> {
writers::new_commit(&self.conn, &mut *self.minter, input)
}
pub fn write_revision(&mut self, input: &NewRevision<'_>) -> Result<(String, i64)> {
writers::write_revision(&self.conn, &mut *self.minter, input)
}
pub fn reconstruct(&self, doc_id: &str) -> Result<Option<String>> {
read::reconstruct(&self.conn, doc_id)
}
pub fn read_at_revision(&self, doc_id: &str, rev_id: &str) -> Result<Option<RevisionRead>> {
read::read_at_revision(&self.conn, doc_id, rev_id)
}
pub fn load_old_match_blocks(&self, doc_id: &str) -> Result<Vec<MatchBlock>> {
read::load_old_match_blocks(&self.conn, doc_id)
}
pub fn load_pool(&self, repo_id: &str, ts: &str) -> Result<Vec<PoolEntry>> {
read::load_pool(&self.conn, repo_id, ts)
}
pub fn rebuild_sections(&self, doc_id: &str) -> Result<()> {
derived::rebuild_sections(&self.conn, doc_id)
}
pub fn rebuild_index(&self, target: RebuildTarget) -> Result<()> {
derived::rebuild_index(&self.conn, target)
}
pub fn gc(&self, enabled: bool) -> Result<GcResult> {
derived::run_gc(&self.conn, enabled)
}
pub fn gc_dry_run(&self) -> Result<GcResult> {
derived::gc_dry_run(&self.conn)
}
pub fn sweep_pool(&self, ts: &str) -> Result<usize> {
derived::sweep_pool(&self.conn, ts)
}
pub fn close(self) -> Result<()> {
self.conn.close().map_err(|(_, e)| Error::Sqlite(e))
}
}
#[cfg(test)]
mod tests;