use super::*;
pub(super) const OPTIMISTIC_ATTEMPTS: u32 = 3;
pub(super) fn is_write_statement(query: &str) -> Result<bool, BoltError> {
let (parsed, is_mutation) = cypher::parse_with_mutation_check(query).map_err(kg_to_bolt)?;
Ok(is_mutation && !parsed.explain)
}
pub(super) fn access_mode_error(scope: &str) -> BoltError {
BoltError::Query {
code: "Neo.ClientError.Statement.AccessMode".into(),
message: format!(
"Writing in read access mode not allowed: this {scope} was opened with mode \"r\" \
(read access). Use a write session or transaction."
),
}
}
impl KgliteBackend {
pub(super) async fn execute_auto_commit(
&self,
query: &str,
parameters: &HashMap<String, BoltValue>,
meta: &TxMeta,
read_mode: bool,
cancel: &kglite::api::session::CancelToken,
) -> Result<ResultStream, BoltError> {
if !is_write_statement(query)? {
return off_async_worker(|| {
let kg_params = decode_params(parameters)?;
let started = Instant::now();
let snapshot = self.session.snapshot();
let mut opts = self.execute_opts(&kg_params, meta);
opts.cancel = Some(cancel.clone());
let outcome = kglite::api::session::execute_read(&snapshot, query, &opts)
.map_err(kg_to_bolt)?;
finish_stream(outcome.result, "r", outcome.explain, started)
});
}
if self.readonly {
return Err(read_only_refusal(
"server is read-only — mutations rejected (--readonly flag)",
));
}
if read_mode {
return Err(access_mode_error("session"));
}
let kg_params = decode_params(parameters)?;
let started = Instant::now();
let mut opts = self.execute_opts(&kg_params, meta);
opts.cancel = Some(cancel.clone());
let (_slot, attempts): (Option<Box<dyn Send>>, u32) =
if self.writer.config().mode == WriteConcurrency::Queue {
if self.session.auto_commit_is_grouped(query, &opts) {
(Some(Box::new(self.auto_commit_shared_slot().await?)), 1)
} else {
(Some(Box::new(self.auto_commit_slot().await?)), 1)
}
} else {
(None, OPTIMISTIC_ATTEMPTS)
};
let result = off_async_worker(|| {
self.session
.execute_auto_commit(query, &opts, attempts)
.map(|outcome| outcome.result)
.map_err(kg_to_bolt_logged)
})?;
let type_str = if is_schema_ddl(query) {
"s"
} else if result.columns.is_empty() {
"w"
} else {
"rw"
};
finish_stream(result, type_str, false, started)
}
async fn auto_commit_shared_slot(&self) -> Result<SharedPermit, BoltError> {
let started = Instant::now();
let permit = self
.writer
.acquire_shared(|holder| self.reclaim_idle_writer(holder))
.await
.map_err(|timed_out| wait_timeout_error("auto-commit", &timed_out))?;
tracing::debug!(
waited_ms = started.elapsed().as_millis() as u64,
"acquired a shared writer permit"
);
Ok(permit)
}
async fn auto_commit_slot(&self) -> Result<(WriterPermit, impl Sized), BoltError> {
let id = self.tx_counter.fetch_add(1, Ordering::Relaxed);
let permit = self
.acquire_writer_slot(&format!("auto-commit-{id}"))
.await?;
let running = permit.activity().begin_query();
Ok((permit, running))
}
}
fn kg_to_bolt_logged(error: kglite::api::KgError) -> BoltError {
if error.code() == kglite::api::KgErrorCode::DurabilityFailed {
tracing::error!(
error = %error,
"auto-commit rejected: the write could not be logged, so it was not applied"
);
}
kg_to_bolt(error)
}