use std::collections::BTreeMap;
use std::rc::Rc;
use omgbase_format::hash::{hex, sha256};
use omgbase_format::json::attrs_to_json;
use omgbase_format::{Attrs, Block, Span};
use rusqlite::{Connection, OptionalExtension, params};
use crate::delta::{
ResolvedList, ResolvedTree, encode_body_delta, encode_frontmatter_delta, entry_text,
full_node_text, is_checkpoint_seq,
};
use crate::error::Result;
use crate::mint::Mint;
use crate::tree::{TreeEntry, from_hex, serialize_tree_entries};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeInputBlock {
pub block_id: String,
pub kind: String,
pub raw: String,
pub text: String,
pub trivia: String,
pub attrs: Attrs,
pub span: Span,
pub children: Vec<TreeInputBlock>,
}
pub fn assign_from_map(
blocks: &[Block],
assignment: &BTreeMap<String, String>,
minter: &mut Mint<'_>,
) -> Result<Vec<TreeInputBlock>> {
fn walk(
list: &[Block],
parent_key: Option<&str>,
assignment: &BTreeMap<String, String>,
minter: &mut Mint<'_>,
) -> Result<Vec<TreeInputBlock>> {
list.iter()
.enumerate()
.map(|(index, b)| {
let key = format!("{}/{index}", parent_key.unwrap_or(""));
let block_id = match assignment.get(&key) {
Some(id) => id.clone(),
None => minter.mint("b")?,
};
Ok(TreeInputBlock {
block_id,
kind: b.kind.as_str().to_owned(),
raw: b.raw.clone(),
text: b.text.clone(),
trivia: b.trivia.clone(),
attrs: b.attrs.clone(),
span: b.span,
children: walk(&b.children, Some(&key), assignment, minter)?,
})
})
.collect()
}
walk(blocks, None, assignment, minter)
}
pub fn assign_fresh_ids(blocks: &[Block], minter: &mut Mint<'_>) -> Result<Vec<TreeInputBlock>> {
assign_from_map(blocks, &BTreeMap::new(), minter)
}
pub fn put_blob(conn: &Connection, text: &str) -> Result<String> {
let bytes = text.as_bytes();
let hash = sha256(bytes);
conn.execute(
"INSERT OR IGNORE INTO blobs (hash, bytes, size) VALUES (?1, ?2, ?3)",
params![&hash[..], bytes, bytes.len() as i64],
)?;
Ok(hex(&hash))
}
pub fn put_node_text(conn: &Connection, text: &str) -> Result<String> {
let hash = sha256(text.as_bytes());
conn.execute(
"INSERT OR IGNORE INTO tree_nodes (hash, entries) VALUES (?1, ?2)",
params![&hash[..], text],
)?;
Ok(hex(&hash))
}
pub fn put_tree_node(conn: &Connection, entries: &[TreeEntry]) -> Result<String> {
put_node_text(conn, &serialize_tree_entries(entries))
}
#[derive(Clone, Debug)]
pub struct TreeWriteContext {
pub base: Option<Rc<ResolvedTree>>,
pub checkpoint: bool,
}
impl TreeWriteContext {
#[must_use]
pub fn checkpoint() -> Self {
Self {
base: None,
checkpoint: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreviousRevision {
pub rev_id: String,
pub seq: i64,
pub root_tree: Option<Vec<u8>>,
pub frontmatter_blob: Option<Vec<u8>>,
pub archive: Option<String>,
}
pub fn previous_revision(conn: &Connection, doc_id: &str) -> Result<Option<PreviousRevision>> {
Ok(conn
.query_row(
"SELECT rev_id, seq, root_tree, frontmatter_blob, archive FROM revisions WHERE doc_id = ?1 ORDER BY seq DESC LIMIT 1",
params![doc_id],
|r| {
Ok(PreviousRevision {
rev_id: r.get(0)?,
seq: r.get(1)?,
root_tree: r.get(2)?,
frontmatter_blob: r.get(3)?,
archive: r.get(4)?,
})
},
)
.optional()?)
}
#[must_use]
pub fn is_checkpoint_revision(seq: i64, previous: Option<&PreviousRevision>) -> bool {
is_checkpoint_seq(seq) || previous.is_none_or(|p| p.archive.is_some())
}
pub fn write_block_tree(conn: &Connection, blocks: &[TreeInputBlock]) -> Result<String> {
write_block_tree_in(conn, blocks, &TreeWriteContext::checkpoint())
}
pub fn write_block_tree_in(
conn: &Connection,
blocks: &[TreeInputBlock],
ctx: &TreeWriteContext,
) -> Result<String> {
write_list(conn, blocks, None, ctx)
}
fn write_list(
conn: &Connection,
blocks: &[TreeInputBlock],
parent: Option<&str>,
ctx: &TreeWriteContext,
) -> Result<String> {
let mut texts = Vec::with_capacity(blocks.len());
for b in blocks {
let raw_hash_hex = put_blob(conn, &b.raw)?;
let trivia_hash_hex = if b.trivia.is_empty() {
None
} else {
Some(put_blob(conn, &b.trivia)?)
};
let child_tree_hash_hex = if b.children.is_empty() {
None
} else {
Some(write_list(conn, &b.children, Some(&b.block_id), ctx)?)
};
texts.push(entry_text(&TreeEntry {
block_id: b.block_id.clone(),
raw_hash_hex,
child_tree_hash_hex,
kind: b.kind.clone(),
attrs: attrs_to_json(&b.attrs),
trivia_hash_hex,
}));
}
let full = full_node_text(&texts);
let base: Option<&Rc<ResolvedList>> = ctx.base.as_ref().and_then(|tree| match parent {
None => Some(&tree.root),
Some(p) => tree.by_block.get(p),
});
if let Some(base) = base {
if base.self_contained && base.texts == texts {
return Ok(base.hash.clone());
}
}
let Some(base) = base.filter(|_| !ctx.checkpoint) else {
return put_node_text(conn, &full);
};
let delta = encode_body_delta(&base.texts, &texts);
put_node_text(
conn,
if delta.len() < full.len() {
&delta
} else {
&full
},
)
}
pub fn write_frontmatter_blob(
conn: &Connection,
raw: &str,
base_raw: Option<&str>,
checkpoint: bool,
) -> Result<String> {
let Some(base_raw) = base_raw.filter(|_| !checkpoint) else {
return put_blob(conn, raw);
};
let delta = encode_frontmatter_delta(base_raw, raw);
put_blob(conn, if delta.len() < raw.len() { &delta } else { raw })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Origin {
Api,
Observed,
Import,
Projection,
}
impl Origin {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Origin::Api => "api",
Origin::Observed => "observed",
Origin::Import => "import",
Origin::Projection => "projection",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewCommit<'a> {
pub repo_id: &'a str,
pub ts: &'a str,
pub origin: Origin,
pub actor: Option<&'a str>,
pub reason: Option<&'a str>,
pub checkpoint_id: Option<&'a str>,
pub ops: Option<&'a str>,
}
impl<'a> NewCommit<'a> {
#[must_use]
pub fn observed(repo_id: &'a str, ts: &'a str) -> Self {
Self {
repo_id,
ts,
origin: Origin::Observed,
actor: None,
reason: None,
checkpoint_id: None,
ops: None,
}
}
}
pub fn new_commit(
conn: &Connection,
minter: &mut Mint<'_>,
input: &NewCommit<'_>,
) -> Result<(String, i64)> {
let commit_id = minter.mint("c")?;
let seq: i64 = conn.query_row(
"SELECT COALESCE(MAX(seq), 0) + 1 FROM commits WHERE repo_id = ?1",
params![input.repo_id],
|r| r.get(0),
)?;
conn.execute(
"INSERT INTO commits (commit_id, repo_id, seq, ts, origin, actor, reason, checkpoint_id, ops)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
commit_id,
input.repo_id,
seq,
input.ts,
input.origin.as_str(),
input.actor,
input.reason,
input.checkpoint_id,
input.ops,
],
)?;
Ok((commit_id, seq))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewRevision<'a> {
pub doc_id: &'a str,
pub root_tree_hex: &'a str,
pub frontmatter_blob_hex: Option<&'a str>,
pub rendered_hash: [u8; 32],
pub path: &'a str,
pub commit_id: &'a str,
pub seq: Option<i64>,
}
pub fn write_revision(
conn: &Connection,
minter: &mut Mint<'_>,
input: &NewRevision<'_>,
) -> Result<(String, i64)> {
let rev_id = minter.mint("r")?;
let seq: i64 = match input.seq {
Some(seq) => seq,
None => conn.query_row(
"SELECT COALESCE(MAX(seq), 0) + 1 FROM revisions WHERE doc_id = ?1",
params![input.doc_id],
|r| r.get(0),
)?,
};
let root_tree = from_hex(input.root_tree_hex)?;
let frontmatter_blob = input.frontmatter_blob_hex.map(from_hex).transpose()?;
conn.execute(
"INSERT INTO revisions (rev_id, doc_id, seq, root_tree, frontmatter_blob, rendered_hash, path, commit_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
rev_id,
input.doc_id,
seq,
root_tree,
frontmatter_blob,
&input.rendered_hash[..],
input.path,
input.commit_id,
],
)?;
Ok((rev_id, seq))
}