use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use anyhow::Result;
use thiserror::Error;
use lora_analyzer::Analyzer;
use lora_compiler::{CompiledQuery, Compiler};
use lora_executor::{
classify_stream, compiled_result_columns, project_rows, ExecuteOptions, ExecutionContext,
Executor, LoraValue, MutableExecutionContext, MutableExecutor, MutablePullExecutor,
PullExecutor, QueryResult, Row, RowSource,
};
use lora_parser::parse_query;
use lora_store::{InMemoryGraph, MutationEvent, MutationRecorder};
use lora_wal::WalRecorder;
use crate::error::LoraError;
use crate::explain::{OperatorMetrics, PlanShape, ProfileMetrics, QueryPlan, QueryProfile};
use crate::live_store::LiveStore;
use crate::snapshot::ManagedSnapshotStore;
use crate::stream::QueryStream;
use crate::wal::write_scope::ensure_wal_not_poisoned;
use lora_compiler::plan_tree_from_compiled;
use lora_executor::{plan_result_columns, CollectorGuard, MetricsCollector};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TransactionError {
#[error("transaction is already closed")]
AlreadyClosed,
#[error("transaction has no live graph guard")]
NoGraphGuard,
#[error("transaction has no staged graph")]
NoStagedGraph,
#[error("cannot commit transaction while a streaming cursor is still active")]
CursorActiveCommit,
#[error("cannot start a new statement while a streaming cursor is still active")]
CursorActiveStatement,
#[error("cannot execute mutating query in read-only transaction")]
ReadOnlyMutation,
#[error("streaming write cursor requires a ReadWrite transaction")]
StreamingRequiresReadWrite,
#[error("read-only transaction cannot publish staged graph")]
ReadOnlyCommit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransactionMode {
ReadOnly,
ReadWrite,
}
pub(crate) enum LiveStoreGuard<'db> {
Read(Arc<InMemoryGraph>),
Write(WriteLease<'db>),
}
pub(crate) struct WriteLease<'db> {
pub(crate) _writer_lock: MutexGuard<'db, ()>,
pub(crate) store: Arc<LiveStore<InMemoryGraph>>,
pub(crate) snapshot: Arc<InMemoryGraph>,
}
impl LiveStoreGuard<'_> {
fn as_graph(&self) -> &InMemoryGraph {
match self {
Self::Read(arc) => arc,
Self::Write(lease) => &lease.snapshot,
}
}
}
pub(crate) struct Savepoint {
staged: Option<InMemoryGraph>,
buffer_len: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TxStreamOutcome {
Exhausted,
Interrupted,
}
impl TxStreamOutcome {
fn should_restore_savepoint(self, rollback_on_drop: bool) -> bool {
matches!(self, Self::Interrupted) && rollback_on_drop
}
}
pub(crate) struct TxCursorLease {
handle: Arc<Mutex<TxInner>>,
rollback_on_drop: bool,
finalized: bool,
}
impl TxCursorLease {
pub(crate) fn new(handle: Arc<Mutex<TxInner>>, rollback_on_drop: bool) -> Self {
Self {
handle,
rollback_on_drop,
finalized: false,
}
}
pub(crate) fn finalize(&mut self, outcome: TxStreamOutcome) {
if self.finalized {
return;
}
finalize_tx_stream(&self.handle, outcome, self.rollback_on_drop);
self.finalized = true;
}
}
impl Drop for TxCursorLease {
fn drop(&mut self) {
self.finalize(TxStreamOutcome::Interrupted);
}
}
pub(crate) struct BufferingRecorder {
buffer: Arc<Mutex<Vec<MutationEvent>>>,
}
impl BufferingRecorder {
pub(crate) fn new(buffer: Arc<Mutex<Vec<MutationEvent>>>) -> Self {
Self { buffer }
}
}
impl MutationRecorder for BufferingRecorder {
fn record(&self, event: MutationEvent) {
if let Ok(mut buf) = self.buffer.lock() {
buf.push(event);
}
}
}
pub(crate) struct TxInner {
pub(crate) staged: Option<InMemoryGraph>,
pub(crate) buffer: Arc<Mutex<Vec<MutationEvent>>>,
pub(crate) pending_savepoint: Option<Savepoint>,
pub(crate) cursor_active: bool,
pub(crate) closed: bool,
pub(crate) mode: TransactionMode,
pub(crate) buffer_mutations: bool,
}
pub struct Transaction<'db> {
pub(crate) live: Option<LiveStoreGuard<'db>>,
pub(crate) inner: Arc<Mutex<TxInner>>,
pub(crate) wal: Option<Arc<WalRecorder>>,
pub(crate) snapshots: Option<Arc<ManagedSnapshotStore>>,
mode: TransactionMode,
}
impl<'db> Transaction<'db> {
pub(crate) fn new(
live: LiveStoreGuard<'db>,
wal: Option<Arc<WalRecorder>>,
snapshots: Option<Arc<ManagedSnapshotStore>>,
mode: TransactionMode,
) -> Self {
let buffer_mutations = wal.is_some();
let inner = TxInner {
staged: None,
buffer: Arc::new(Mutex::new(Vec::new())),
pending_savepoint: None,
cursor_active: false,
closed: false,
mode,
buffer_mutations,
};
Self {
live: Some(live),
inner: Arc::new(Mutex::new(inner)),
wal,
snapshots,
mode,
}
}
pub fn mode(&self) -> TransactionMode {
self.mode
}
pub fn execute(
&mut self,
query: &str,
options: Option<ExecuteOptions>,
) -> Result<QueryResult, LoraError> {
self.execute_with_params(query, options, BTreeMap::new())
}
pub fn execute_with_timeout(
&mut self,
query: &str,
options: Option<ExecuteOptions>,
timeout: Duration,
) -> Result<QueryResult, LoraError> {
let deadline = Instant::now()
.checked_add(timeout)
.unwrap_or_else(Instant::now);
let rows =
self.execute_rows_with_params_deadline(query, BTreeMap::new(), Some(deadline))?;
Ok(project_rows(rows, options.unwrap_or_default()))
}
pub fn execute_with_params(
&mut self,
query: &str,
options: Option<ExecuteOptions>,
params: BTreeMap<String, LoraValue>,
) -> Result<QueryResult, LoraError> {
let rows = self.execute_rows_with_params_deadline(query, params, None)?;
Ok(project_rows(rows, options.unwrap_or_default()))
}
pub fn execute_with_params_timeout(
&mut self,
query: &str,
options: Option<ExecuteOptions>,
params: BTreeMap<String, LoraValue>,
timeout: Duration,
) -> Result<QueryResult, LoraError> {
let deadline = Instant::now()
.checked_add(timeout)
.unwrap_or_else(Instant::now);
let rows = self.execute_rows_with_params_deadline(query, params, Some(deadline))?;
Ok(project_rows(rows, options.unwrap_or_default()))
}
pub fn execute_rows(&mut self, query: &str) -> Result<Vec<Row>, LoraError> {
self.execute_rows_with_params(query, BTreeMap::new())
}
pub fn execute_rows_with_params(
&mut self,
query: &str,
params: BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>, LoraError> {
Ok(self.execute_rows_with_params_deadline(query, params, None)?)
}
fn execute_rows_with_params_deadline(
&mut self,
query: &str,
params: BTreeMap<String, LoraValue>,
deadline: Option<Instant>,
) -> Result<Vec<Row>> {
let compiled = self.compile_in_tx(query)?;
self.execute_rows_compiled_deadline(&compiled, params, deadline)
}
fn execute_rows_compiled_deadline(
&mut self,
compiled: &CompiledQuery,
params: BTreeMap<String, LoraValue>,
deadline: Option<Instant>,
) -> Result<Vec<Row>> {
if self.is_read_only_unchecked() {
self.precheck_open_no_savepoint()?;
let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
let storage = live.as_graph();
let executor = Executor::with_deadline(ExecutionContext { storage, params }, deadline);
return executor
.execute_compiled_rows(compiled)
.map_err(anyhow::Error::from);
}
let mut inner = self.begin_statement()?;
let is_mutating = classify_stream(compiled).is_mutating();
if !is_mutating {
return match inner.staged.as_ref() {
Some(staged) => {
let executor = Executor::with_deadline(
ExecutionContext {
storage: staged,
params,
},
deadline,
);
executor
.execute_compiled_rows(compiled)
.map_err(anyhow::Error::from)
}
None => {
drop(inner);
let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
let storage = live.as_graph();
let executor =
Executor::with_deadline(ExecutionContext { storage, params }, deadline);
executor
.execute_compiled_rows(compiled)
.map_err(anyhow::Error::from)
}
};
}
let clone_savepoint_graph = inner.staged.is_some();
self.ensure_staged_locked(&mut inner)?;
let savepoint = Some(take_savepoint(&inner, clone_savepoint_graph));
let exec_result: ExecResultRows = {
let staged = inner.staged_mut()?;
let mut executor = MutableExecutor::with_deadline(
MutableExecutionContext {
storage: staged,
params,
},
deadline,
);
executor
.execute_compiled_rows(compiled)
.map_err(anyhow::Error::from)
};
match exec_result {
Ok(rows) => Ok(rows),
Err(err) => {
restore_savepoint(&mut inner, savepoint);
Err(err)
}
}
}
pub(crate) fn open_streaming_compiled_autocommit(
&mut self,
compiled: Arc<CompiledQuery>,
params: BTreeMap<String, LoraValue>,
) -> Result<Box<dyn RowSource + 'static>> {
if self.is_read_only_unchecked() {
return Err(TransactionError::StreamingRequiresReadWrite.into());
}
let mut inner = self.begin_statement()?;
self.ensure_staged_locked(&mut inner)?;
inner.activate_cursor();
let staged_ptr: *mut InMemoryGraph = inner
.staged
.as_mut()
.expect("ensure_staged_locked guarantees Some")
as *mut _;
drop(inner);
let storage_static: &'static mut InMemoryGraph = unsafe { &mut *staged_ptr };
let compiled_static: &'static CompiledQuery =
unsafe { std::mem::transmute::<&CompiledQuery, _>(compiled.as_ref()) };
let cursor = MutablePullExecutor::new(storage_static, params)
.open_compiled(compiled_static)
.map_err(|e| {
if let Ok(mut inner) = self.inner.lock() {
discard_transaction_state(&mut inner);
}
self.live.take();
anyhow::Error::from(e)
})?;
Ok(Box::new(StreamingCursorWithArc {
cursor,
_compiled: compiled,
}))
}
fn compile_in_tx(&self, query: &str) -> Result<CompiledQuery> {
let document = parse_query(query)?;
let resolved = {
let inner = self.lock_inner_unchecked();
if let Some(staged) = &inner.staged {
let mut analyzer = Analyzer::new(staged);
analyzer.analyze(&document)?
} else {
drop(inner);
let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
let mut analyzer = Analyzer::new(live.as_graph());
analyzer.analyze(&document)?
}
};
Ok(Compiler::compile(&resolved))
}
fn ensure_staged_locked(&self, inner: &mut MutexGuard<'_, TxInner>) -> Result<()> {
if inner.staged.is_some() {
return Ok(());
}
let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
let mut staged: InMemoryGraph = live.as_graph().clone();
if matches!(inner.mode, TransactionMode::ReadWrite) && inner.buffer_mutations {
staged.set_mutation_recorder(Some(
Arc::new(BufferingRecorder::new(inner.buffer.clone())) as Arc<dyn MutationRecorder>,
));
}
inner.staged = Some(staged);
Ok(())
}
pub fn explain(
&self,
query: &str,
_params: Option<BTreeMap<String, LoraValue>>,
) -> Result<QueryPlan, LoraError> {
let compiled = self.compile_in_tx(query).map_err(LoraError::from_anyhow)?;
let tree = plan_tree_from_compiled(&compiled);
let shape: PlanShape = classify_stream(&compiled).into();
let result_columns = plan_result_columns(&compiled.physical);
Ok(QueryPlan {
query: query.to_string(),
tree,
shape,
result_columns,
})
}
pub fn profile(
&mut self,
query: &str,
params: Option<BTreeMap<String, LoraValue>>,
) -> Result<QueryProfile, LoraError> {
let params = params.unwrap_or_default();
let compiled = self.compile_in_tx(query).map_err(LoraError::from_anyhow)?;
let tree = plan_tree_from_compiled(&compiled);
let shape: PlanShape = classify_stream(&compiled).into();
let result_columns = plan_result_columns(&compiled.physical);
let plan = QueryPlan {
query: query.to_string(),
tree,
shape,
result_columns,
};
let collector = Arc::new(MetricsCollector::new());
let _guard = CollectorGuard::install(collector.clone());
let started = Instant::now();
let rows = self
.execute_rows_compiled_deadline(&compiled, params, None)
.map_err(LoraError::from_anyhow)?;
let total_elapsed_ns = started.elapsed().as_nanos() as u64;
drop(_guard);
let per_operator = collector
.snapshot()
.into_iter()
.map(|(id, op)| {
(
id,
OperatorMetrics {
rows: op.rows,
elapsed_ns: op.elapsed_ns,
next_calls: op.next_calls,
db_hits: 0,
},
)
})
.collect();
let metrics = ProfileMetrics {
total_elapsed_ns,
total_rows: rows.len() as u64,
mutated: shape.is_mutating(),
per_operator,
};
Ok(QueryProfile { plan, metrics })
}
pub fn stream(&mut self, query: &str) -> Result<QueryStream<'static>, LoraError> {
self.stream_with_params(query, BTreeMap::new())
}
pub fn stream_with_params(
&mut self,
query: &str,
params: BTreeMap<String, LoraValue>,
) -> Result<QueryStream<'static>, LoraError> {
let compiled = Arc::new(self.compile_in_tx(query)?);
let columns = compiled_result_columns(&compiled);
Ok(self.stream_compiled(compiled, columns, params)?)
}
pub(crate) fn stream_compiled(
&mut self,
compiled: Arc<CompiledQuery>,
columns: Vec<String>,
params: BTreeMap<String, LoraValue>,
) -> Result<QueryStream<'static>> {
let mut inner = self.begin_statement()?;
let is_mutating = classify_stream(&compiled).is_mutating();
if matches!(inner.mode, TransactionMode::ReadOnly) && is_mutating {
return Err(TransactionError::ReadOnlyMutation.into());
}
let clone_savepoint_graph = inner.staged.is_some();
self.ensure_staged_locked(&mut inner)?;
inner.activate_cursor();
let rollback_on_drop = is_mutating;
if rollback_on_drop {
inner.pending_savepoint = Some(take_savepoint(&inner, clone_savepoint_graph));
} else {
inner.pending_savepoint = None;
}
let staged_ptr: *mut InMemoryGraph = inner
.staged
.as_mut()
.expect("ensure_staged_locked guarantees Some")
as *mut _;
drop(inner);
let compiled_static: &'static CompiledQuery =
unsafe { std::mem::transmute::<&CompiledQuery, _>(compiled.as_ref()) };
let cursor: Result<Box<dyn RowSource + 'static>> = if is_mutating {
let storage_static: &'static mut InMemoryGraph = unsafe { &mut *staged_ptr };
MutablePullExecutor::new(storage_static, params)
.open_compiled(compiled_static)
.map(|cursor| {
Box::new(StreamingCursorWithArc {
cursor,
_compiled: compiled.clone(),
}) as Box<dyn RowSource + 'static>
})
.map_err(anyhow::Error::from)
} else {
let storage_static: &'static InMemoryGraph = unsafe { &*staged_ptr };
PullExecutor::new(storage_static, params)
.open_compiled(compiled_static)
.map(|cursor| {
Box::new(StreamingCursorWithArc {
cursor,
_compiled: compiled.clone(),
}) as Box<dyn RowSource + 'static>
})
.map_err(anyhow::Error::from)
};
match cursor {
Ok(cursor) => Ok(QueryStream::for_tx_cursor(
cursor,
columns,
TxCursorLease::new(self.inner.clone(), rollback_on_drop),
)),
Err(err) => {
finalize_tx_stream(&self.inner, TxStreamOutcome::Interrupted, rollback_on_drop);
Err(err)
}
}
}
pub fn commit(mut self) -> Result<(), LoraError> {
let CommitState {
staged,
buffer_events,
mode,
} = self.take_commit_state()?;
let wrote_wal_commit = self.replay_commit_wal(mode, buffer_events)?;
self.publish_staged_graph(mode, staged, wrote_wal_commit)?;
self.live.take();
Ok(())
}
fn take_commit_state(&self) -> Result<CommitState> {
let mut inner = self.inner.lock().unwrap();
if inner.cursor_active {
return Err(TransactionError::CursorActiveCommit.into());
}
if inner.closed {
return Err(TransactionError::AlreadyClosed.into());
}
let mode = inner.mode;
let staged = inner.staged.take();
let buffer_events = std::mem::take(&mut *inner.buffer.lock().unwrap());
inner.closed = true;
Ok(CommitState {
staged,
buffer_events,
mode,
})
}
fn replay_commit_wal(
&self,
mode: TransactionMode,
buffer_events: Vec<MutationEvent>,
) -> Result<bool> {
let Some(rec) = &self.wal else {
return Ok(false);
};
if !matches!(mode, TransactionMode::ReadWrite) {
ensure_wal_not_poisoned(rec)?;
return Ok(false);
}
Ok(rec.commit_events(buffer_events)?.wrote())
}
fn publish_staged_graph(
&mut self,
mode: TransactionMode,
staged: Option<InMemoryGraph>,
wrote_wal_commit: bool,
) -> Result<()> {
if !matches!(mode, TransactionMode::ReadWrite) {
return Ok(());
}
let Some(mut staged) = staged else {
return Ok(());
};
staged.set_mutation_recorder(None);
let wal = self.wal.clone();
if let Some(rec) = &wal {
staged.set_mutation_recorder(Some(rec.clone() as Arc<dyn MutationRecorder>));
}
let live = self.live.as_mut().ok_or(TransactionError::NoGraphGuard)?;
let lease = match live {
LiveStoreGuard::Write(lease) => lease,
LiveStoreGuard::Read(_) => {
return Err(TransactionError::ReadOnlyCommit.into());
}
};
if wrote_wal_commit {
if let (Some(snapshots), Some(rec)) = (&self.snapshots, wal.as_ref()) {
snapshots.observe_commit(&staged, rec)?;
}
}
lease.store.store(Arc::new(staged));
Ok(())
}
pub fn rollback(mut self) -> Result<(), LoraError> {
let mut inner = self.inner.lock().unwrap();
if inner.closed {
return Err(TransactionError::AlreadyClosed.into());
}
discard_transaction_state(&mut inner);
drop(inner);
self.live.take();
Ok(())
}
fn begin_statement(&self) -> Result<MutexGuard<'_, TxInner>> {
let inner = self.inner.lock().unwrap();
if inner.closed {
return Err(TransactionError::AlreadyClosed.into());
}
if inner.cursor_active {
return Err(TransactionError::CursorActiveStatement.into());
}
Ok(inner)
}
fn precheck_open_no_savepoint(&self) -> Result<()> {
let inner = self.inner.lock().unwrap();
if inner.closed {
return Err(TransactionError::AlreadyClosed.into());
}
if inner.cursor_active {
return Err(TransactionError::CursorActiveStatement.into());
}
Ok(())
}
fn is_read_only_unchecked(&self) -> bool {
matches!(self.mode, TransactionMode::ReadOnly)
}
fn lock_inner_unchecked(&self) -> MutexGuard<'_, TxInner> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn release_streaming_cursor(&self) {
if let Ok(mut inner) = self.inner.lock() {
inner.release_cursor();
}
}
}
type ExecResultRows = Result<Vec<Row>>;
struct CommitState {
staged: Option<InMemoryGraph>,
buffer_events: Vec<MutationEvent>,
mode: TransactionMode,
}
impl TxInner {
fn staged_mut(&mut self) -> Result<&mut InMemoryGraph> {
self.staged
.as_mut()
.ok_or(TransactionError::NoStagedGraph.into())
}
fn activate_cursor(&mut self) {
self.cursor_active = true;
}
fn release_cursor(&mut self) {
self.cursor_active = false;
}
fn clear_pending_savepoint(&mut self) {
self.pending_savepoint = None;
}
fn restore_pending_savepoint(&mut self) {
if let Some(sp) = self.pending_savepoint.take() {
apply_savepoint(self, sp);
}
}
fn finalize_stream(&mut self, outcome: TxStreamOutcome, rollback_on_drop: bool) {
self.release_cursor();
if self.closed {
discard_transaction_state(self);
return;
}
if outcome.should_restore_savepoint(rollback_on_drop) {
self.restore_pending_savepoint();
} else {
self.clear_pending_savepoint();
}
}
}
struct StreamingCursorWithArc {
cursor: Box<dyn RowSource + 'static>,
_compiled: Arc<CompiledQuery>,
}
impl RowSource for StreamingCursorWithArc {
fn next_row(&mut self) -> lora_executor::ExecResult<Option<Row>> {
self.cursor.next_row()
}
}
fn finalize_tx_stream(
handle: &Arc<Mutex<TxInner>>,
outcome: TxStreamOutcome,
rollback_on_drop: bool,
) {
if let Ok(mut inner) = handle.lock() {
inner.finalize_stream(outcome, rollback_on_drop);
}
}
fn discard_transaction_state(inner: &mut TxInner) {
inner.clear_pending_savepoint();
inner.release_cursor();
inner.staged = None;
if let Ok(mut buf) = inner.buffer.lock() {
buf.clear();
}
inner.closed = true;
}
fn take_savepoint(inner: &TxInner, clone_staged: bool) -> Savepoint {
let buffer_len = inner.buffer.lock().ok().map(|b| b.len()).unwrap_or(0);
Savepoint {
staged: if clone_staged {
inner.staged.as_ref().cloned()
} else {
None
},
buffer_len,
}
}
fn restore_savepoint(inner: &mut TxInner, savepoint: Option<Savepoint>) {
if let Some(sp) = savepoint {
apply_savepoint(inner, sp);
}
}
fn apply_savepoint(inner: &mut TxInner, sp: Savepoint) {
if let Ok(mut buf) = inner.buffer.lock() {
buf.truncate(sp.buffer_len);
}
let Some(mut graph) = sp.staged else {
inner.staged = None;
return;
};
if matches!(inner.mode, TransactionMode::ReadWrite) && inner.buffer_mutations {
graph.set_mutation_recorder(Some(
Arc::new(BufferingRecorder::new(inner.buffer.clone())) as Arc<dyn MutationRecorder>
));
}
inner.staged = Some(graph);
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if let Ok(mut inner) = self.inner.lock() {
if !inner.closed {
if inner.cursor_active {
inner.closed = true;
} else {
discard_transaction_state(&mut inner);
}
}
}
}
}