use omgbase_format::hash::sha256;
use rusqlite::{Connection, OptionalExtension, params};
use crate::Store;
use crate::doc_store::DocStore;
use crate::error::Result;
use crate::read::{read_at_revision, reconstruct};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PendingKind {
Write,
Rename,
Remove,
}
impl PendingKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
PendingKind::Write => "write",
PendingKind::Rename => "rename",
PendingKind::Remove => "remove",
}
}
fn parse(s: &str) -> Option<Self> {
match s {
"write" => Some(PendingKind::Write),
"rename" => Some(PendingKind::Rename),
"remove" => Some(PendingKind::Remove),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingRow {
pub path: String,
pub kind: PendingKind,
pub from_path: Option<String>,
pub doc_id: String,
pub rev_id: Option<String>,
pub rendered_hash: Vec<u8>,
pub commit_id: String,
}
const COLUMNS: &str = "path, kind, from_path, doc_id, rev_id, rendered_hash, commit_id";
fn row_from(r: &rusqlite::Row<'_>) -> rusqlite::Result<PendingRow> {
let kind: String = r.get(1)?;
Ok(PendingRow {
path: r.get(0)?,
kind: PendingKind::parse(&kind).unwrap_or(PendingKind::Write),
from_path: r.get(2)?,
doc_id: r.get(3)?,
rev_id: r.get(4)?,
rendered_hash: r.get(5)?,
commit_id: r.get(6)?,
})
}
pub(crate) fn pending_rows(conn: &Connection, repo_id: &str) -> Result<Vec<PendingRow>> {
let mut stmt = conn.prepare(&format!(
"SELECT {COLUMNS} FROM pending_materializations WHERE repo_id = ?1 ORDER BY rowid"
))?;
let rows = stmt.query_map(params![repo_id], row_from)?;
Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
}
pub(crate) fn pending_at(
conn: &Connection,
repo_id: &str,
path: &str,
) -> Result<Option<PendingRow>> {
Ok(conn
.query_row(
&format!(
"SELECT {COLUMNS} FROM pending_materializations WHERE repo_id = ?1 AND path = ?2"
),
params![repo_id, path],
row_from,
)
.optional()?)
}
pub(crate) fn insert_pending(conn: &Connection, repo_id: &str, row: &PendingRow) -> Result<()> {
conn.execute(
"INSERT OR REPLACE INTO pending_materializations
(repo_id, path, kind, from_path, doc_id, rev_id, rendered_hash, commit_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
repo_id,
row.path,
row.kind.as_str(),
row.from_path,
row.doc_id,
row.rev_id,
row.rendered_hash,
row.commit_id,
],
)?;
Ok(())
}
pub(crate) fn delete_pending(conn: &Connection, repo_id: &str, path: &str) -> Result<()> {
conn.execute(
"DELETE FROM pending_materializations WHERE repo_id = ?1 AND path = ?2",
params![repo_id, path],
)?;
Ok(())
}
fn previous_rendered_hash(
conn: &Connection,
doc_id: &str,
rev_id: &str,
) -> Result<Option<Vec<u8>>> {
Ok(conn
.query_row(
"SELECT rendered_hash FROM revisions
WHERE doc_id = ?1 AND seq < (SELECT seq FROM revisions WHERE rev_id = ?2)
ORDER BY seq DESC LIMIT 1",
params![doc_id, rev_id],
|r| r.get(0),
)
.optional()?)
}
fn bytes_for(conn: &Connection, row: &PendingRow) -> Result<Option<String>> {
let bytes = match &row.rev_id {
Some(rev) => read_at_revision(conn, &row.doc_id, rev)?.map(|r| r.content),
None => reconstruct(conn, &row.doc_id)?,
};
Ok(bytes.filter(|b| hashes_to(b, &row.rendered_hash)))
}
fn hashes_to(bytes: &str, hash: &[u8]) -> bool {
sha256(bytes.as_bytes())[..] == hash[..]
}
fn replaceable(conn: &Connection, row: &PendingRow, bytes: &str) -> Result<bool> {
if hashes_to(bytes, &row.rendered_hash) {
return Ok(true);
}
match &row.rev_id {
Some(rev) => {
Ok(previous_rendered_hash(conn, &row.doc_id, rev)?
.is_some_and(|h| hashes_to(bytes, &h)))
}
None => Ok(false),
}
}
fn materialize_row(
conn: &Connection,
row: &PendingRow,
doc_store: &mut dyn DocStore,
) -> Result<bool> {
match row.kind {
PendingKind::Write => match doc_store.read(&row.path)? {
Some(on_disk) if hashes_to(&on_disk, &row.rendered_hash) => Ok(true),
Some(on_disk) if !replaceable(conn, row, &on_disk)? => Ok(false),
_ => {
let Some(bytes) = bytes_for(conn, row)? else {
return Ok(false);
};
doc_store.write(&row.path, &bytes)?;
Ok(true)
}
},
PendingKind::Rename => {
let from = row.from_path.as_deref();
if let Some(at_to) = doc_store.read(&row.path)? {
if !hashes_to(&at_to, &row.rendered_hash) {
return Ok(false);
}
remove_if_replaceable(conn, row, from, doc_store)?;
return Ok(true);
}
if let Some(from) = from.filter(|f| doc_store.exists(f)) {
match doc_store.rename(from, &row.path) {
Ok(()) => {
if let Some(moved) = doc_store.read(&row.path)? {
if !hashes_to(&moved, &row.rendered_hash)
&& replaceable(conn, row, &moved)?
{
if let Some(bytes) = bytes_for(conn, row)? {
doc_store.write(&row.path, &bytes)?;
}
}
}
Ok(true)
}
Err(_) => {
let Some(bytes) = bytes_for(conn, row)? else {
return Ok(false);
};
doc_store.write(&row.path, &bytes)?;
remove_if_replaceable(conn, row, Some(from), doc_store)?;
Ok(true)
}
}
} else {
let Some(bytes) = bytes_for(conn, row)? else {
return Ok(false);
};
doc_store.write(&row.path, &bytes)?;
Ok(true)
}
}
PendingKind::Remove => {
if let Some(on_disk) = doc_store.read(&row.path)? {
if hashes_to(&on_disk, &row.rendered_hash) {
doc_store.remove(&row.path)?;
}
}
Ok(true)
}
}
}
fn remove_if_replaceable(
conn: &Connection,
row: &PendingRow,
from: Option<&str>,
doc_store: &mut dyn DocStore,
) -> Result<()> {
let Some(from) = from else {
return Ok(());
};
if let Some(bytes) = doc_store.read(from)? {
if replaceable(conn, row, &bytes)? {
doc_store.remove(from)?;
}
}
Ok(())
}
impl Store {
pub fn pending_materializations(&self, repo_id: &str) -> Result<Vec<PendingRow>> {
pending_rows(&self.conn, repo_id)
}
pub fn materialize_pending(
&mut self,
repo_id: &str,
doc_store: &mut dyn DocStore,
) -> Result<Vec<String>> {
let mut done = Vec::new();
for row in pending_rows(&self.conn, repo_id)? {
if materialize_row(&self.conn, &row, doc_store)? {
delete_pending(&self.conn, repo_id, &row.path)?;
done.push(row.path);
}
}
Ok(done)
}
pub(crate) fn materialize_paths(
&mut self,
repo_id: &str,
paths: &[String],
doc_store: &mut dyn DocStore,
) -> Result<Vec<String>> {
let mut done = Vec::new();
for path in paths {
let Some(row) = pending_at(&self.conn, repo_id, path)? else {
continue;
};
if materialize_row(&self.conn, &row, doc_store)? {
delete_pending(&self.conn, repo_id, path)?;
done.push(path.clone());
}
}
Ok(done)
}
}