mod codec;
mod dialect;
mod facts;
#[cfg(feature = "postgres")]
mod postgres;
mod reads;
mod schema;
#[cfg(feature = "sqlite")]
mod sqlite;
mod statements;
mod write;
use std::fmt;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use sha2::{Digest, Sha256};
pub use dialect::Dialect;
#[cfg(feature = "postgres")]
pub use postgres::Postgres;
#[cfg(feature = "sqlite")]
pub use sqlite::Sqlite;
use self::codec::Row;
use self::dialect::{Batch, Class, Mode, Statement, Value, Write};
use self::statements::Catalogue;
use super::read::{
AccountRow, BalanceRead, EntryRef, Erasure, HoldCursor, HoldRead, HoldRow, Leaf, LeafRead,
LedgerInfo, Memo, Page, PageRead, Position, PostingCursor, PostingLine, PostingRead, SealRow,
StoredEntry,
};
use super::{ProjectionPage, ProjectionRead, Store, StoreError};
use crate::core::{BalanceKey, Done, Op};
use crate::error::Rejection;
use crate::log::Hash;
use crate::model::{
AccountHandle, AccountRef, Balance, Currency, EntryId, Genesis, Layer, LedgerId, PeriodId,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SqlOptions {
pub schema: String,
pub max_connections: u32,
pub busy_timeout: Duration,
}
impl Default for SqlOptions {
fn default() -> Self {
Self {
schema: String::from("doubleentry"),
max_connections: 10,
busy_timeout: Duration::from_secs(5),
}
}
}
pub struct SqlStore<D: Dialect, const P: u8 = 2> {
inner: Arc<Inner<D>>,
}
struct Inner<D: Dialect> {
pool: D::Pool,
sql: Catalogue,
schema: String,
ledger: OnceLock<LedgerLock>,
retries: u32,
}
#[cfg(feature = "postgres")]
impl<const P: u8> SqlStore<Postgres, P> {
pub fn with_schema(&self, schema: &str) -> Result<Self, StoreError> {
self.sharing_pool(schema)
}
}
#[derive(Debug, Clone)]
struct LedgerLock {
id: LedgerId,
key: i64,
}
impl<D: Dialect, const P: u8> Clone for SqlStore<D, P> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<D: Dialect, const P: u8> fmt::Debug for SqlStore<D, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqlStore")
.field("schema", &self.inner.schema)
.finish_non_exhaustive()
}
}
fn lock_key(tag: &str, name: &str) -> i64 {
let digest = Sha256::new()
.chain_update(tag.as_bytes())
.chain_update(name.as_bytes())
.finalize();
let mut key = [0u8; 8];
for (slot, byte) in key.iter_mut().zip(digest.iter()) {
*slot = *byte;
}
i64::from_be_bytes(key)
}
impl<D: Dialect, const P: u8> SqlStore<D, P> {
async fn connect(url: &str, options: SqlOptions) -> Result<Self, StoreError> {
let qualifier = D::qualifier(&options.schema)?;
let pool = D::connect(url, &options).await?;
Ok(Self {
inner: Arc::new(Inner {
pool,
sql: Catalogue::new(qualifier),
schema: options.schema,
ledger: OnceLock::new(),
retries: options.max_connections,
}),
})
}
#[cfg_attr(not(feature = "postgres"), allow(dead_code))]
fn sharing_pool(&self, schema: &str) -> Result<Self, StoreError> {
let qualifier = D::qualifier(schema)?;
Ok(Self {
inner: Arc::new(Inner {
pool: self.inner.pool.clone(),
sql: Catalogue::new(qualifier),
schema: schema.to_owned(),
ledger: OnceLock::new(),
retries: self.inner.retries,
}),
})
}
pub async fn close(&self) {
D::close(&self.inner.pool).await;
}
async fn session(
&self,
mode: Mode,
operation: &'static str,
) -> Result<Session<'_, D>, StoreError> {
let mut retries = self.inner.retries;
let tx = loop {
match D::begin(&self.inner.pool, mode).await {
Ok(tx) => break tx,
Err(error) if retries > 0 && Class::of(&error, D::classify) == Class::Lost => {
retries = retries.saturating_sub(1);
}
Err(error) => {
return Err(failure::<D>(operation, &error, false).with_source(error));
}
}
};
Ok(Session {
tx,
operation,
sql: &self.inner.sql,
rows: 0,
})
}
async fn ledger(&self, session: &mut Session<'_, D>) -> Result<LedgerLock, StoreError> {
if let Some(ledger) = self.inner.ledger.get() {
return Ok(ledger.clone());
}
let sql = session.sql;
let row = session
.first(&sql.shared.meta, Vec::new())
.await?
.ok_or_else(|| StoreError::not_created(session.operation))?;
let id = LedgerId::new(row.text(1)?).map_err(|_| {
StoreError::integrity(session.operation, "the ledger id is invalid", None)
})?;
Ok(self.remember(id))
}
fn remember(&self, id: LedgerId) -> LedgerLock {
self.inner
.ledger
.get_or_init(|| LedgerLock {
key: lock_key(
"doubleentry/ledger-lock",
&format!("{}\u{0}{}", self.inner.schema, id.as_str()),
),
id,
})
.clone()
}
fn create_key(&self) -> i64 {
lock_key("doubleentry/create", &self.inner.schema)
}
}
pub(crate) struct Session<'s, D: Dialect> {
tx: D::Tx,
operation: &'static str,
sql: &'s Catalogue,
rows: u64,
}
fn failure<D: Dialect>(
operation: &'static str,
error: &sqlx::Error,
at_commit: bool,
) -> StoreError {
let (failure, unknown) = match Class::of(error, D::classify) {
Class::Busy => (StoreError::unavailable(operation), false),
Class::Lost => (StoreError::unavailable(operation), true),
Class::Integrity => (
StoreError::integrity(operation, "the database refused the change", None),
false,
),
Class::MissingTable => (StoreError::not_created(operation), false),
Class::Failed => (StoreError::failed(operation), false),
Class::Broken => (StoreError::failed(operation), true),
};
if at_commit && unknown {
failure.outcome_unknown()
} else {
failure
}
}
impl<D: Dialect> Session<'_, D> {
fn fail(&self, error: sqlx::Error) -> StoreError {
failure::<D>(self.operation, &error, false).with_source(error)
}
async fn fetch(
&mut self,
statement: &Statement,
args: Vec<Value>,
) -> Result<Vec<Row>, StoreError> {
let rows = D::fetch(&mut self.tx, statement, args)
.await
.map_err(|error| self.fail(error))?;
self.count(rows.len());
Ok(rows
.into_iter()
.map(|values| Row::new(self.operation, values))
.collect())
}
async fn fetch_values(
&mut self,
statement: &Statement,
args: Vec<Value>,
) -> Result<Vec<Vec<Value>>, StoreError> {
let rows = D::fetch(&mut self.tx, statement, args)
.await
.map_err(|error| self.fail(error))?;
self.count(rows.len());
Ok(rows)
}
async fn first(
&mut self,
statement: &Statement,
args: Vec<Value>,
) -> Result<Option<Row>, StoreError> {
Ok(self.fetch(statement, args).await?.into_iter().next())
}
async fn execute(
&mut self,
statement: &Statement,
args: Vec<Value>,
) -> Result<u64, StoreError> {
D::execute(&mut self.tx, statement, args)
.await
.map_err(|error| self.fail(error))
}
async fn batch(
&mut self,
batch: &Batch,
rows: Vec<Vec<Value>>,
) -> Result<Vec<Vec<Value>>, StoreError> {
if rows.is_empty() {
return Ok(Vec::new());
}
let found = D::batch(&mut self.tx, batch, rows)
.await
.map_err(|error| self.fail(error))?;
self.count(found.len());
Ok(found)
}
async fn apply(&mut self, writes: Vec<Write<'_>>) -> Result<(), StoreError> {
D::apply(&mut self.tx, writes)
.await
.map_err(|error| self.fail(error))
}
async fn keyed(
&mut self,
lookup: &Batch,
keys: Vec<Vec<Value>>,
) -> Result<Vec<Vec<Row>>, StoreError> {
let mut grouped: Vec<Vec<Row>> = keys.iter().map(|_| Vec::new()).collect();
let rows = keys
.into_iter()
.zip(0i64..)
.map(|(mut key, index)| {
key.insert(0, Value::Int(index));
key
})
.collect();
for mut values in self.batch(lookup, rows).await? {
let index = match values.first() {
Some(Value::Int(index)) => usize::try_from(*index).ok(),
_ => None,
};
let slot = index
.and_then(|index| grouped.get_mut(index))
.ok_or_else(|| {
StoreError::integrity(self.operation, "a batched row names no key", None)
})?;
values.remove(0);
slot.push(Row::new(self.operation, values));
}
Ok(grouped)
}
async fn lock(&mut self, key: i64) -> Result<(), Option<StoreError>> {
match D::lock(&mut self.tx, key).await {
Ok(()) => Ok(()),
Err(error) if Class::of(&error, D::classify) == Class::Lost => Err(None),
Err(error) => Err(Some(self.fail(error))),
}
}
async fn script(&mut self, script: String) -> Result<(), StoreError> {
D::script(&mut self.tx, script)
.await
.map_err(|error| self.fail(error))
}
async fn size(&mut self) -> Result<u64, StoreError> {
let sql = self.sql;
match self.first(&sql.shared.size, Vec::new()).await? {
Some(row) => row.u64(0),
None => Ok(0),
}
}
async fn commit(self) -> Result<(), StoreError> {
let operation = self.operation;
D::commit(self.tx)
.await
.map_err(|error| failure::<D>(operation, &error, true).with_source(error))
}
async fn rollback(self) -> Result<(), StoreError> {
let operation = self.operation;
D::rollback(self.tx)
.await
.map_err(|error| failure::<D>(operation, &error, false).with_source(error))
}
fn count(&mut self, rows: usize) {
self.rows = self
.rows
.saturating_add(u64::try_from(rows).unwrap_or(u64::MAX));
}
}
impl<D: Dialect, const P: u8> Store<P> for SqlStore<D, P> {
async fn create(&self, genesis: &Genesis) -> Result<LedgerInfo, StoreError> {
write::create(self, genesis).await
}
async fn open(&self) -> Result<LedgerInfo, StoreError> {
write::open(self).await
}
async fn write(&self, op: &Op<P>) -> Result<Result<Done<P>, Rejection>, StoreError> {
write::write(self, op).await
}
async fn size(&self) -> Result<u64, StoreError> {
let mut session = self.session(Mode::Read, "size").await?;
let size = session.size().await?;
session.commit().await?;
Ok(size)
}
async fn read_leaves(&self, read: &LeafRead) -> Result<Page<Leaf, Position>, StoreError> {
reads::read_leaves(self, read).await
}
async fn read_nodes(&self, ids: &[u64]) -> Result<Vec<Hash>, StoreError> {
reads::read_nodes(self, ids).await
}
async fn seals(&self, through: Option<u64>) -> Result<Vec<SealRow>, StoreError> {
reads::seals(self, through).await
}
async fn accounts(&self, refs: &[AccountRef]) -> Result<Vec<Option<AccountRow>>, StoreError> {
reads::accounts(self, refs).await
}
async fn account_records(
&self,
handle: AccountHandle,
page: PageRead<Position>,
) -> Result<Page<Leaf, Position>, StoreError> {
reads::account_records(self, handle, &page).await
}
async fn entry(
&self,
by: EntryRef<'_>,
through: Option<u64>,
) -> Result<Option<StoredEntry>, StoreError> {
reads::entry(self, by, through).await
}
async fn holds(&self, read: &HoldRead) -> Result<Page<HoldRow<P>, HoldCursor>, StoreError> {
reads::holds(self, read).await
}
async fn clearings(
&self,
key: BalanceKey,
page: PageRead<Position>,
) -> Result<Page<Leaf, Position>, StoreError> {
reads::clearings(self, key, &page).await
}
async fn projection(&self, read: &ProjectionRead) -> Result<ProjectionPage<P>, StoreError> {
reads::projection(self, read).await
}
async fn seal_nodes(
&self,
period: &PeriodId,
currency: Currency,
layer: Layer,
ids: &[(u32, u32)],
) -> Result<Option<Vec<Option<Hash>>>, StoreError> {
reads::seal_nodes(self, period, currency, layer, ids)
.await
.map(Some)
}
async fn balances(
&self,
read: &BalanceRead,
) -> Result<Page<(BalanceKey, Balance<P>), BalanceKey>, StoreError> {
reads::balances(self, read).await
}
async fn postings(
&self,
read: &PostingRead,
) -> Result<Page<PostingLine<P>, PostingCursor>, StoreError> {
reads::postings(self, read).await
}
async fn memo(&self, id: EntryId) -> Result<Option<Memo>, StoreError> {
reads::memo(self, id).await
}
async fn erase_memo(&self, id: EntryId) -> Result<Erasure, StoreError> {
write::erase_memo(self, id).await
}
}
#[cfg(test)]
mod tests {
use super::lock_key;
#[test]
fn lock_keys_differ_per_ledger_and_purpose() {
let a = lock_key("doubleentry/ledger-lock", "example.com/a");
assert_ne!(a, lock_key("doubleentry/ledger-lock", "example.com/b"));
assert_ne!(a, lock_key("doubleentry/create", "example.com/a"));
assert_eq!(a, lock_key("doubleentry/ledger-lock", "example.com/a"));
}
}