use std::any::{Any, TypeId};
#[cfg(not(target_family = "wasm"))]
use std::collections::HashMap;
#[cfg(target_family = "wasm")]
use std::collections::HashSet;
#[cfg(not(target_family = "wasm"))]
use std::collections::hash_map::Entry;
use std::fmt::{self, Display};
#[cfg(storage)]
use std::path::PathBuf;
use std::pin::pin;
use std::sync::Arc;
use std::task::{Poll, ready};
use std::time::Duration;
#[allow(unused_imports)]
use anyhow::bail;
use anyhow::{Context as _, Result, ensure};
use async_channel::Sender;
use bytes::{Bytes, BytesMut};
use futures::{Future, Stream};
use rand::Rng;
use reblessive::TreeStack;
use surrealdb_types::{AuthError, Error as TypesError, SurrealValue, object};
#[cfg(not(target_family = "wasm"))]
use tokio::spawn;
use tokio::sync::Notify;
use tokio::time::{Instant, sleep, timeout, timeout_at};
use tokio_util::sync::CancellationToken;
use tracing::{debug, instrument, trace, warn};
use uuid::Uuid;
use super::api::{BoxFut, Transactable};
use super::tr::Transactor;
use super::tx::Transaction;
use super::version::MajorVersion;
use super::{Key, Val, export};
use crate::api::err::ApiError;
use crate::api::invocation::process_api_request;
use crate::api::request::ApiRequest;
use crate::api::response::ApiResponse;
use crate::buc::manager::BucketsManager;
use crate::catalog::providers::{
ApiProvider, CatalogProvider, DatabaseProvider, NamespaceProvider, NodeProvider, TableProvider,
UserProvider,
};
use crate::catalog::{ApiDefinition, Index, NodeLiveQuery, SubscriptionDefinition};
use crate::cnf::dynamic::DynamicConfiguration;
use crate::cnf::{CommonConfig, ConfigMap, LiveQueryEngine};
use crate::ctx::{CancelHandle, Context};
#[cfg(feature = "jwks")]
use crate::dbs::capabilities::NetTarget;
use crate::dbs::capabilities::{
ArbitraryQueryTarget, EvalQueryTarget, ExperimentalTarget, MethodTarget, RouteTarget,
};
use crate::dbs::node::{Node, Timestamp};
use crate::dbs::{
Capabilities, DurableSession, Executor, MessageBroker, Options, QueryResult,
QueryResultBuilder, Session,
};
use crate::doc::AsyncEventRecord;
use crate::err::Error;
use crate::exec::function::FunctionRegistry;
use crate::expr::model::get_model_path;
use crate::expr::statements::{DefineModelStatement, DefineStatement, DefineUserStatement};
use crate::expr::{Base, Expr, FlowResultExt as _, Literal, LogicalPlan, TopLevelExpr};
#[cfg(feature = "gql")]
use crate::gql::PreparedGqlQuery;
#[cfg(feature = "http")]
use crate::http::HttpClient;
use crate::iam::{Action, Auth, Error as IamError, Resource, ResourceKind, Role};
use crate::idx::IndexKeyBase;
use crate::idx::index::IndexOperation;
use crate::idx::trees::store::IndexStores;
use crate::key::root::ic::IndexCompactionKey;
use crate::key::root::rc::{
RECLAIM_DATABASE, RECLAIM_INDEX, RECLAIM_NAMESPACE, ReclaimKey, ReclaimState,
};
use crate::kvs::LockType::*;
use crate::kvs::TransactionType::*;
use crate::kvs::cache::ds::DatastoreCache;
use crate::kvs::clock::SystemClock;
use crate::kvs::ds::requirements::{
TransactionBuilderFactoryRequirements, TransactionBuilderRequirements,
};
use crate::kvs::index::IndexBuilder;
use crate::kvs::sequences::Sequences;
use crate::kvs::slowlog::SlowLog;
use crate::kvs::tasklease::{LeaseHandler, TaskLeaseType};
#[cfg(test)]
use crate::kvs::testing::{RetryableConflictSite, maybe_inject_retryable_conflict};
use crate::kvs::{
Error as KvsError, KVValue, LockType, NORMAL_BATCH_SIZE, TransactionType,
is_retryable_transaction_conflict,
};
use crate::lq::LiveQueryRouter;
use crate::observe::{ExecutionObserver, NoopObserver};
use crate::sql::Ast;
#[cfg(feature = "surrealism")]
use crate::surrealism::cache::SurrealismCache;
use crate::syn::parser::{ParserSettings, StatementStream};
use crate::types::{PublicNotification, PublicValue, PublicVariables};
use crate::val::convert_value_to_public_value;
use crate::{CommunityComposer, syn};
mod builder;
pub use builder::Builder;
const TARGET: &str = "surrealdb::core::kvs::ds";
const NODE_DELETE_TIMEOUT: Duration = Duration::from_secs(60);
const INITIAL_USER_ROLE: &str = "owner";
fn is_conditional_write_conflict(err: &anyhow::Error) -> bool {
fn is_condition_error(e: &KvsError) -> bool {
matches!(e, KvsError::TransactionConditionNotMet | KvsError::TransactionKeyAlreadyExists)
}
if let Some(e) = err.downcast_ref::<KvsError>() {
return is_condition_error(e);
}
matches!(err.downcast_ref::<Error>(), Some(Error::Kvs(e)) if is_condition_error(e))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShutdownNodeDeleteOutcome {
Archived,
Failed,
TimedOut,
}
async fn await_node_step<T, Fut>(
deadline: Instant,
timeout_duration: Duration,
canceller: Option<&CancellationToken>,
step: Fut,
) -> Result<T>
where
Fut: Future<Output = Result<T>>,
{
if let Some(canceller) = canceller {
tokio::select! {
biased;
_ = canceller.cancelled() => bail!(Error::QueryCancelled),
result = timeout_at(deadline, step) => match result {
Ok(result) => result,
Err(_) => bail!(Error::QueryTimedout(timeout_duration.into())),
},
}
} else {
match timeout_at(deadline, step).await {
Ok(result) => result,
Err(_) => bail!(Error::QueryTimedout(timeout_duration.into())),
}
}
}
async fn await_node_tx_step<T, Fut>(
txn: &Transaction,
deadline: Instant,
timeout_duration: Duration,
canceller: Option<&CancellationToken>,
step: Fut,
) -> Result<T>
where
Fut: Future<Output = Result<T>>,
{
let result = if let Some(canceller) = canceller {
tokio::select! {
biased;
_ = canceller.cancelled() => {
let _ = txn.cancel().await;
bail!(Error::QueryCancelled);
}
result = timeout_at(deadline, step) => result,
}
} else {
timeout_at(deadline, step).await
};
match result {
Ok(Ok(value)) => Ok(value),
Ok(Err(e)) => {
let _ = txn.cancel().await;
Err(e)
}
Err(_) => {
let _ = txn.cancel().await;
bail!(Error::QueryTimedout(timeout_duration.into()))
}
}
}
fn archive_node_for_shutdown(
timeout_duration: Duration,
result: Result<()>,
) -> ShutdownNodeDeleteOutcome {
match result {
Ok(()) => ShutdownNodeDeleteOutcome::Archived,
Err(e) => {
if matches!(e.downcast_ref::<Error>(), Some(Error::QueryTimedout(_))) {
warn!(
target: TARGET,
timeout = ?timeout_duration,
"Timed out archiving node during shutdown; continuing shutdown"
);
return ShutdownNodeDeleteOutcome::TimedOut;
}
warn!(
target: TARGET,
error = %e,
"Failed to archive node during shutdown; continuing shutdown"
);
ShutdownNodeDeleteOutcome::Failed
}
}
}
pub struct Datastore {
transaction_factory: TransactionFactory,
id: Uuid,
auth_enabled: bool,
dynamic_configuration: DynamicConfiguration,
slow_log: Option<SlowLog>,
transaction_timeout: Option<Duration>,
capabilities: Arc<Capabilities>,
live_query_broker: Option<Arc<dyn MessageBroker>>,
live_query_router: Arc<LiveQueryRouter>,
http_endpoint: Option<String>,
index_stores: IndexStores,
cache: Arc<DatastoreCache>,
#[cfg(all(feature = "graphql", not(target_family = "wasm")))]
graphql_schema_cache: crate::graphql::cache::GraphQLSchemaCache,
function_registry: Arc<FunctionRegistry>,
index_builder: IndexBuilder,
#[cfg(storage)]
temporary_directory: Option<Arc<PathBuf>>,
buckets: BucketsManager,
sequences: Sequences,
#[cfg(feature = "surrealism")]
surrealism_cache: Arc<SurrealismCache>,
#[cfg(feature = "surrealism")]
lazy_surrealism: bool,
async_event_trigger: Arc<Notify>,
config: Arc<CommonConfig>,
#[cfg(feature = "http")]
http_client: Arc<HttpClient>,
observer: Arc<dyn ExecutionObserver>,
}
pub struct Metrics {
pub name: &'static str,
pub u64_metrics: Vec<Metric>,
}
pub struct Metric {
pub name: &'static str,
pub description: &'static str,
}
#[derive(Clone)]
pub(crate) struct TransactionFactory {
builder: Arc<Box<dyn TransactionBuilder>>,
async_event_trigger: Arc<Notify>,
observer: Arc<dyn ExecutionObserver>,
config: Arc<CommonConfig>,
}
impl TransactionFactory {
pub(super) fn new(
async_event_trigger: Arc<Notify>,
builder: Box<dyn TransactionBuilder>,
config: Arc<CommonConfig>,
) -> Self {
Self {
builder: Arc::new(builder),
async_event_trigger,
observer: Arc::new(NoopObserver),
config,
}
}
pub(crate) fn with_observer(mut self, observer: Arc<dyn ExecutionObserver>) -> Self {
self.observer = observer;
self
}
#[allow(dead_code)]
pub(crate) fn observer(&self) -> &Arc<dyn ExecutionObserver> {
&self.observer
}
#[allow(
unreachable_code,
unreachable_patterns,
unused_variables,
reason = "Some variables are unused when no backends are enabled."
)]
pub async fn transaction(
&self,
write: TransactionType,
lock: LockType,
sequences: Sequences,
) -> Result<Transaction> {
let write = match write {
Read => false,
Write => true,
};
let lock = match lock {
Pessimistic => true,
Optimistic => false,
};
let (inner, local) = self.builder.new_transaction(write, lock).await?;
Ok(Transaction::new(
local,
sequences,
Arc::clone(&self.async_event_trigger),
Arc::clone(&self.observer),
Transactor {
inner,
},
&self.config,
))
}
fn register_metrics(&self) -> Option<Metrics> {
self.builder.register_metrics()
}
fn collect_u64_metric(&self, metric: &str) -> Option<u64> {
self.builder.collect_u64_metric(metric)
}
}
pub trait TransactionBuilder: TransactionBuilderRequirements {
fn new_transaction(
&self,
write: bool,
lock: bool,
) -> BoxFut<'_, Result<(Box<dyn Transactable>, bool)>>;
fn shutdown(&self) -> BoxFut<'_, Result<()>>;
fn register_metrics(&self) -> Option<Metrics>;
fn collect_u64_metric(&self, metric: &str) -> Option<u64>;
fn extension(&self, _: TypeId) -> Option<Arc<dyn Any + Send + Sync>> {
None
}
}
pub struct TransactionBuilderParts<S> {
pub builder: Box<dyn TransactionBuilder>,
pub router_state: S,
}
impl<S> TransactionBuilderParts<S> {
pub fn new(builder: Box<dyn TransactionBuilder>, router_state: S) -> Self {
Self {
builder,
router_state,
}
}
}
impl TransactionBuilderParts<()> {
pub fn without_router_state(builder: Box<dyn TransactionBuilder>) -> Self {
Self::new(builder, ())
}
}
pub trait TransactionBuilderFactory: TransactionBuilderFactoryRequirements {
type RouterState: Clone + Send + Sync + 'static;
#[cfg(not(target_family = "wasm"))]
fn new_transaction_builder(
&self,
path: &str,
canceller: CancellationToken,
config: ConfigMap,
) -> impl Future<Output = Result<TransactionBuilderParts<Self::RouterState>>> + Send;
#[cfg(target_family = "wasm")]
fn new_transaction_builder(
&self,
path: &str,
canceller: CancellationToken,
config: ConfigMap,
) -> impl Future<Output = Result<TransactionBuilderParts<Self::RouterState>>>;
fn path_valid(v: &str) -> Result<String>;
fn datastore_node_id(&self) -> Option<[u8; 16]> {
None
}
fn live_query_broker(&self, channel: Sender<PublicNotification>) -> Arc<dyn MessageBroker> {
crate::dbs::LocalMessageBroker::new(channel)
}
fn http_endpoint(&self) -> Option<String> {
None
}
}
pub mod requirements {
use std::fmt::Display;
#[cfg(target_family = "wasm")]
pub trait TransactionBuilderRequirements: Display {}
#[cfg(not(target_family = "wasm"))]
pub trait TransactionBuilderRequirements: Display + Send + Sync + 'static {}
#[cfg(target_family = "wasm")]
pub trait TransactionBuilderFactoryRequirements {}
#[cfg(not(target_family = "wasm"))]
pub trait TransactionBuilderFactoryRequirements: Send + Sync + 'static {}
}
pub enum DatastoreFlavor {
#[cfg(feature = "kv-mem")]
Mem(super::mem::Datastore),
#[cfg(feature = "kv-rocksdb")]
RocksDB(super::rocksdb::Datastore),
#[cfg(feature = "kv-indxdb")]
IndxDB(super::indxdb::Datastore),
#[cfg(feature = "kv-tikv")]
TiKV(super::tikv::Datastore),
#[cfg(feature = "kv-surrealkv")]
SurrealKV(super::surrealkv::Datastore),
}
impl TransactionBuilderFactoryRequirements for CommunityComposer {}
impl TransactionBuilderFactory for CommunityComposer {
type RouterState = ();
#[allow(unused_variables)]
async fn new_transaction_builder(
&self,
path: &str,
_canceller: CancellationToken,
config: ConfigMap,
) -> Result<TransactionBuilderParts<Self::RouterState>> {
let (raw_path, config_string) = match path.split_once('?') {
Some((p, q)) => (p, Some(q)),
None => (path, None),
};
let config = if let Some(config_string) = config_string {
config.join(
ConfigMap::from_config_string(config_string).map_keys(|x| format!("datastore_{x}")),
)
} else {
config
};
let (flavour, path) = match raw_path.split_once("://").or_else(|| raw_path.split_once(':'))
{
None if raw_path == "memory" => ("memory", ""),
None if raw_path == "mem" => ("memory", ""),
Some(("mem", path)) => ("memory", path),
Some((flavour, path)) => (flavour, path),
_ => bail!(Error::Unreachable("Provide a valid database path parameter".to_owned())),
};
let path = if path.starts_with("/") {
let normalised = format!("/{}", path.trim_start_matches("/"));
info!(target: TARGET, "Starting kvs store at absolute path {flavour}:{normalised}");
normalised
} else if path.is_empty() {
info!(target: TARGET, "Starting kvs store in memory");
"".to_string()
} else {
info!(target: TARGET, "Starting kvs store at relative path {flavour}://{path}");
path.to_string()
};
match (flavour, path) {
(flavour @ "memory", path) => {
#[cfg(feature = "kv-mem")]
{
super::threadpool::initialise();
let config = if path.is_empty() {
config
} else {
config.with_key_value("datastore_persist", path)
};
let config = config.load();
let v = super::mem::Datastore::new(config).await.map(DatastoreFlavor::Mem)?;
info!(target: TARGET, "Started kvs store in {flavour}");
Ok(TransactionBuilderParts::without_router_state(Box::<DatastoreFlavor>::new(
v,
)))
}
#[cfg(not(feature = "kv-mem"))]
bail!(Error::Kvs(crate::kvs::Error::Datastore("Cannot connect to the `memory` storage engine as it is not enabled in this build of SurrealDB".to_owned())));
}
("file", _) => {
bail!(Error::Kvs(crate::kvs::Error::Datastore(
"The `file://` scheme is no longer supported; use `rocksdb://` or `surrealkv://` instead"
.into()
)));
}
(flavour @ "rocksdb", path) => {
#[cfg(feature = "kv-rocksdb")]
{
super::threadpool::initialise();
let config = config.load();
let v = super::rocksdb::Datastore::new(&path, config)
.await
.map(DatastoreFlavor::RocksDB)?;
info!(target: TARGET, "Started {flavour} kvs store");
Ok(TransactionBuilderParts::without_router_state(Box::<DatastoreFlavor>::new(
v,
)))
}
#[cfg(not(feature = "kv-rocksdb"))]
bail!(Error::Kvs(crate::kvs::Error::Datastore("Cannot connect to the `rocksdb` storage engine as it is not enabled in this build of SurrealDB".to_owned())));
}
(flavour @ "surrealkv", path) => {
#[cfg(feature = "kv-surrealkv")]
{
super::threadpool::initialise();
let config = config.load();
let v = super::surrealkv::Datastore::new(&path, config)
.await
.map(DatastoreFlavor::SurrealKV)?;
info!(target: TARGET, "Started {flavour} kvs store");
Ok(TransactionBuilderParts::without_router_state(Box::<DatastoreFlavor>::new(
v,
)))
}
#[cfg(not(feature = "kv-surrealkv"))]
bail!(Error::Kvs(crate::kvs::Error::Datastore("Cannot connect to the `surrealkv` storage engine as it is not enabled in this build of SurrealDB".to_owned())));
}
(flavour @ "indxdb", path) => {
#[cfg(feature = "kv-indxdb")]
{
let v =
super::indxdb::Datastore::new(&path).await.map(DatastoreFlavor::IndxDB)?;
info!(target: TARGET, "Started {flavour} kvs store");
Ok(TransactionBuilderParts::without_router_state(Box::<DatastoreFlavor>::new(
v,
)))
}
#[cfg(not(feature = "kv-indxdb"))]
bail!(Error::Kvs(crate::kvs::Error::Datastore("Cannot connect to the `indxdb` storage engine as it is not enabled in this build of SurrealDB".to_owned())));
}
(flavour @ "tikv", path) => {
#[cfg(feature = "kv-tikv")]
{
let tikv_config = config.load();
let v = super::tikv::Datastore::new(&path, tikv_config)
.await
.map(DatastoreFlavor::TiKV)?;
info!(target: TARGET, "Started {flavour} kvs store");
Ok(TransactionBuilderParts::without_router_state(Box::<DatastoreFlavor>::new(
v,
)))
}
#[cfg(not(feature = "kv-tikv"))]
bail!(Error::Kvs(crate::kvs::Error::Datastore("Cannot connect to the `tikv` storage engine as it is not enabled in this build of SurrealDB".to_owned())));
}
(flavour, path) => {
info!(target: TARGET, "Unable to load the specified datastore {flavour}{path}");
bail!(Error::Kvs(crate::kvs::Error::Datastore(
"Unable to load the specified datastore".into()
)))
}
}
}
fn path_valid(v: &str) -> Result<String> {
let scheme_part = v.split_once('?').map(|(s, _)| s).unwrap_or(v);
match scheme_part {
"memory" => Ok(v.to_string()),
"mem" => Ok(v.to_string()),
v_s if v_s.starts_with("file:") => Ok(v.to_string()),
v_s if v_s.starts_with("rocksdb:") => Ok(v.to_string()),
v_s if v_s.starts_with("surrealkv:") => Ok(v.to_string()),
v_s if v_s.starts_with("mem:") => Ok(v.to_string()),
v_s if v_s.starts_with("tikv:") => Ok(v.to_string()),
_ => bail!("Provide a valid database path parameter"),
}
}
}
impl TransactionBuilderRequirements for DatastoreFlavor {}
impl TransactionBuilder for DatastoreFlavor {
#[allow(
unreachable_code,
unreachable_patterns,
unused_variables,
reason = "Some variables are unused when no backends are enabled."
)]
fn new_transaction(
&self,
write: bool,
lock: bool,
) -> BoxFut<'_, Result<(Box<dyn Transactable>, bool)>> {
Box::pin(async move {
Ok(match self {
#[cfg(feature = "kv-mem")]
Self::Mem(v) => {
let tx = v.transaction(write, lock).await?;
(tx, true)
}
#[cfg(feature = "kv-rocksdb")]
Self::RocksDB(v) => {
let tx = v.transaction(write, lock).await?;
(tx, true)
}
#[cfg(feature = "kv-indxdb")]
Self::IndxDB(v) => {
let tx = v.transaction(write, lock).await?;
(tx, true)
}
#[cfg(feature = "kv-tikv")]
Self::TiKV(v) => {
let tx = v.transaction(write, lock).await?;
(tx, false)
}
#[cfg(feature = "kv-surrealkv")]
Self::SurrealKV(v) => {
let tx = v.transaction(write, lock).await?;
(tx, true)
}
_ => unreachable!(),
})
})
}
fn register_metrics(&self) -> Option<Metrics> {
match self {
#[cfg(feature = "kv-rocksdb")]
DatastoreFlavor::RocksDB(v) => Some(v.register_metrics()),
#[allow(unreachable_patterns)]
_ => None,
}
}
#[allow(unused_variables)]
fn collect_u64_metric(&self, metric: &str) -> Option<u64> {
match self {
#[cfg(feature = "kv-rocksdb")]
DatastoreFlavor::RocksDB(v) => v.collect_u64_metric(metric),
#[allow(unreachable_patterns)]
_ => None,
}
}
fn shutdown(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
match self {
#[cfg(feature = "kv-mem")]
Self::Mem(v) => Ok(v.shutdown().await?),
#[cfg(feature = "kv-rocksdb")]
Self::RocksDB(v) => Ok(v.shutdown().await?),
#[cfg(feature = "kv-indxdb")]
Self::IndxDB(v) => Ok(v.shutdown().await?),
#[cfg(feature = "kv-tikv")]
Self::TiKV(v) => Ok(v.shutdown().await?),
#[cfg(feature = "kv-surrealkv")]
Self::SurrealKV(v) => Ok(v.shutdown().await?),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
})
}
#[allow(
unused_variables,
reason = "type_id is only consumed when a backend feature is enabled"
)]
fn extension(&self, type_id: TypeId) -> Option<Arc<dyn Any + Send + Sync>> {
match self {
#[cfg(feature = "kv-tikv")]
Self::TiKV(v) if type_id == TypeId::of::<super::tikv::TikvOpsHandle>() => Some(v.ops_handle()),
#[allow(unreachable_patterns)]
_ => None,
}
}
}
impl Display for DatastoreFlavor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#![allow(unused_variables)]
match self {
#[cfg(feature = "kv-mem")]
Self::Mem(_) => write!(f, "memory"),
#[cfg(feature = "kv-rocksdb")]
Self::RocksDB(_) => write!(f, "rocksdb"),
#[cfg(feature = "kv-indxdb")]
Self::IndxDB(_) => write!(f, "indxdb"),
#[cfg(feature = "kv-tikv")]
Self::TiKV(_) => write!(f, "tikv"),
#[cfg(feature = "kv-surrealkv")]
Self::SurrealKV(_) => write!(f, "surrealkv"),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}
}
impl Display for Datastore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.transaction_factory.builder.fmt(f)
}
}
impl Datastore {
pub fn builder() -> Builder {
Builder::new()
}
async fn retry_index_operation_conflict(
err: &anyhow::Error,
operation: impl Into<String>,
) -> bool {
if is_retryable_transaction_conflict(err) {
let operation = operation.into();
debug!(
target: TARGET,
operation = %operation,
error = %err,
"retryable index operation conflict, retrying"
);
sleep(Duration::from_millis(100)).await;
true
} else {
false
}
}
async fn cancel_and_retry_index_operation_conflict(
txn: &Transaction,
err: &anyhow::Error,
operation: impl Into<String>,
) -> bool {
let _ = txn.cancel().await;
Self::retry_index_operation_conflict(err, operation).await
}
pub async fn new(path: &str) -> Result<Self> {
Builder::new().build_with_path(path).await
}
pub fn register_metrics(&self) -> Option<Metrics> {
self.transaction_factory.register_metrics()
}
pub fn collect_u64_metric(&self, metric: &str) -> Option<u64> {
self.transaction_factory.collect_u64_metric(metric)
}
pub fn observer(&self) -> &Arc<dyn ExecutionObserver> {
&self.observer
}
pub fn restart(self) -> Self {
self.buckets.clear();
Self {
id: self.id,
auth_enabled: self.auth_enabled,
dynamic_configuration: DynamicConfiguration::default(),
slow_log: self.slow_log,
transaction_timeout: self.transaction_timeout,
capabilities: Arc::clone(&self.capabilities),
live_query_broker: self.live_query_broker,
live_query_router: Arc::new(LiveQueryRouter::new()),
http_endpoint: self.http_endpoint,
index_stores: IndexStores::new(
self.config.hnsw_cache_size,
self.config.diskann_cache_size,
),
index_builder: IndexBuilder::new(self.transaction_factory.clone()),
#[cfg(storage)]
temporary_directory: self.temporary_directory,
cache: Arc::new(DatastoreCache::new(self.config.datastore_cache_size)),
#[cfg(all(feature = "graphql", not(target_family = "wasm")))]
graphql_schema_cache: crate::graphql::cache::GraphQLSchemaCache::default(),
function_registry: Arc::new(FunctionRegistry::with_builtins()),
buckets: self.buckets,
sequences: Sequences::new(self.transaction_factory.clone(), self.id),
transaction_factory: self.transaction_factory,
async_event_trigger: self.async_event_trigger,
#[cfg(feature = "surrealism")]
surrealism_cache: Arc::new(SurrealismCache::new(self.config.surrealism_cache_size)),
#[cfg(feature = "surrealism")]
lazy_surrealism: self.lazy_surrealism,
#[cfg(feature = "http")]
http_client: self.http_client,
observer: self.observer,
config: self.config,
}
}
#[cfg(test)]
#[cfg_attr(not(feature = "kv-mem"), allow(dead_code))]
pub(crate) fn fork_for_test_with_node_id(&self, id: Uuid) -> Self {
let transaction_factory = self.transaction_factory.clone();
Self {
id,
auth_enabled: self.auth_enabled,
dynamic_configuration: self.dynamic_configuration.clone(),
slow_log: self.slow_log.clone(),
transaction_timeout: self.transaction_timeout,
capabilities: Arc::clone(&self.capabilities),
live_query_broker: self.live_query_broker.clone(),
live_query_router: Arc::new(LiveQueryRouter::new()),
http_endpoint: self.http_endpoint.clone(),
index_stores: IndexStores::new(
self.config.hnsw_cache_size,
self.config.diskann_cache_size,
),
index_builder: IndexBuilder::new(transaction_factory.clone()),
#[cfg(storage)]
temporary_directory: self.temporary_directory.clone(),
cache: Arc::new(DatastoreCache::new(self.config.datastore_cache_size)),
#[cfg(all(feature = "graphql", not(target_family = "wasm")))]
graphql_schema_cache: crate::graphql::cache::GraphQLSchemaCache::default(),
function_registry: Arc::new(FunctionRegistry::with_builtins()),
buckets: self.buckets.clone(),
sequences: Sequences::new(transaction_factory.clone(), id),
transaction_factory,
async_event_trigger: Arc::clone(&self.async_event_trigger),
#[cfg(feature = "surrealism")]
surrealism_cache: Arc::new(SurrealismCache::new(self.config.surrealism_cache_size)),
#[cfg(feature = "surrealism")]
lazy_surrealism: self.lazy_surrealism,
#[cfg(feature = "http")]
http_client: Arc::clone(&self.http_client),
observer: Arc::clone(&self.observer),
config: Arc::clone(&self.config),
}
}
pub fn with_node_id(mut self, id: Uuid) -> Self {
self.id = id;
self
}
pub fn with_transaction_timeout(mut self, duration: Option<Duration>) -> Self {
self.transaction_timeout = duration;
self
}
pub(crate) fn transaction_timeout(&self) -> Option<Duration> {
self.transaction_timeout
}
pub(crate) fn live_query_broker(&self) -> Option<Arc<dyn MessageBroker>> {
self.live_query_broker.clone()
}
pub async fn lookup_node_endpoint(&self, node_id: Uuid) -> Result<Option<String>> {
let txn = self.transaction(Read, Optimistic).await?;
let key = crate::key::root::nd::Nd::new(node_id);
let res = txn.get(&key, None).await?;
let _ = txn.cancel().await;
Ok(res.and_then(|node: Node| node.http_endpoint))
}
#[cfg(storage)]
pub fn with_temporary_directory(mut self, path: Option<PathBuf>) -> Self {
self.temporary_directory = path.map(Arc::new);
self
}
#[cfg(feature = "surrealism")]
pub fn with_lazy_surrealism(mut self, lazy: bool) -> Self {
self.lazy_surrealism = lazy;
self
}
#[cfg(feature = "surrealism")]
pub fn is_lazy_surrealism(&self) -> bool {
self.lazy_surrealism
}
pub fn index_store(&self) -> &IndexStores {
&self.index_stores
}
pub fn is_auth_enabled(&self) -> bool {
self.auth_enabled
}
pub fn id(&self) -> Uuid {
self.id
}
pub(crate) fn allows_rpc_method(&self, method_target: &MethodTarget) -> bool {
self.capabilities.allows_rpc_method(method_target)
}
pub fn allows_http_route(&self, route_target: &RouteTarget) -> bool {
self.capabilities.allows_http_route(route_target)
}
pub fn allows_query_by_subject(&self, subject: impl Into<ArbitraryQueryTarget>) -> bool {
self.capabilities.allows_query(&subject.into())
}
pub fn allows_eval_query_by_subject(&self, subject: impl Into<EvalQueryTarget>) -> bool {
self.capabilities.allows_eval_query(&subject.into())
}
#[cfg(feature = "jwks")]
pub(crate) fn allows_network_target(&self, net_target: &NetTarget) -> bool {
self.capabilities.allows_network_target(net_target)
}
pub fn get_capabilities(&self) -> &Capabilities {
&self.capabilities
}
#[cfg(feature = "jwks")]
pub(crate) fn cache(&self) -> &Arc<DatastoreCache> {
&self.cache
}
pub(super) fn clock_now(&self) -> Timestamp {
SystemClock::new().now()
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn check_version(&self) -> Result<(MajorVersion, bool)> {
let (version, is_new) = Self::retry("Check version", || self.get_version()).await?;
if !version.is_latest() {
bail!(Error::OutdatedStorageVersion {
expected: MajorVersion::latest().into(),
actual: version.into(),
});
}
Ok((version, is_new))
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn get_version(&self) -> Result<(MajorVersion, bool)> {
let txn = self.transaction(Write, Optimistic).await?.enclose();
let key = crate::key::version::new();
let val = match catch!(txn, txn.get(&key, None).await) {
Some(val) => {
catch!(txn, txn.cancel().await);
(val, false)
}
None => {
let rng = crate::key::version::proceeding();
let keys = catch!(txn, txn.keys(rng, 1, 0, None).await);
let version = if keys.is_empty() {
MajorVersion::latest()
} else {
warn!(
target: TARGET,
first_key = ?keys.first().map(|k| format!("{:?}", k)),
"No version key found but existing data detected in storage. \
This storage contains data from a previous SurrealDB version. \
The server will not start until the data is migrated or removed."
);
MajorVersion::v1()
};
catch!(txn, txn.replace(&key, &version).await);
catch!(txn, txn.commit().await);
(version, true)
}
};
Ok(val)
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn initialise_credentials(&self, user: &str, pass: &str) -> Result<()> {
Self::retry("Initialise credentials", || self.initialise_credentials_attempt(user, pass))
.await
}
async fn initialise_credentials_attempt(&self, user: &str, pass: &str) -> Result<()> {
let txn = self.transaction(Write, Optimistic).await?.enclose();
let users = catch!(txn, txn.all_root_users(None).await);
if users.is_empty() {
info!(target: TARGET, "Credentials were provided, and no root users were found. The root user '{user}' will be created");
let stm = DefineUserStatement::new_with_password(
Base::Root,
user.to_owned(),
pass,
INITIAL_USER_ROLE.to_owned(),
);
let opt = Options::new(&CommonConfig::default())
.with_auth(Arc::new(Auth::for_root(Role::Owner)));
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
let mut stack = TreeStack::new();
let res = stack.enter(|stk| stm.compute(stk, &ctx, &opt, None)).finish().await;
catch!(txn, res);
txn.commit().await
} else {
warn!(target: TARGET, "Credentials were provided, but existing root users were found. The root user '{user}' will not be created");
warn!(target: TARGET, "Consider removing the --user and --pass arguments from the server start command");
txn.cancel().await
}
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn initialise_defaults(&self, namespace: &str, database: &str) -> Result<()> {
info!(target: TARGET, "This is a new SurrealDB instance. Initialising default namespace '{namespace}' and database '{database}'");
let sql = r"
DEFINE NAMESPACE $namespace COMMENT 'Default namespace generated by SurrealDB';
USE NS $namespace;
DEFINE DATABASE $database COMMENT 'Default database generated by SurrealDB';
DEFINE CONFIG DEFAULT NAMESPACE $namespace DATABASE $database;
"
.to_string();
let vars = map! {
"namespace".to_string() => namespace.to_string().into_value(),
"database".to_string() => database.to_string().into_value(),
};
self.execute(
&sql,
&Session::owner(),
Some(vars.into_iter().collect::<std::collections::BTreeMap<_, _>>().into()),
)
.await?;
Ok(())
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn startup(&self, sql: &str, sess: &Session) -> Result<Vec<QueryResult>> {
trace!(target: TARGET, "Running datastore startup import script");
ensure!(!sess.expired(), Error::ExpiredSession);
self.execute(sql, sess, None).await.map_err(|e| anyhow::anyhow!(e))
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn shutdown(&self) -> Result<()> {
trace!(target: TARGET, "Running datastore shutdown operations");
let _ = archive_node_for_shutdown(
NODE_DELETE_TIMEOUT,
self.delete_node_with_timeout(NODE_DELETE_TIMEOUT).await,
);
self.transaction_factory.builder.shutdown().await
}
pub async fn unsafe_destroy_range(&self, start: Vec<u8>, end: Vec<u8>) -> Result<()> {
#[cfg(feature = "kv-tikv")]
if let Some(ops) = self.tikv_ops() {
return ops.unsafe_destroy_range(start, end).await.map_err(Into::into);
}
let _ = (start, end);
Ok(())
}
pub async fn run_mvcc_gc(&self, lifetime: Duration) -> Result<()> {
#[cfg(feature = "kv-tikv")]
if let Some(ops) = self.tikv_ops() {
return ops.run_mvcc_gc(lifetime).await.map_err(Into::into);
}
let _ = lifetime;
Ok(())
}
pub async fn run_lock_cleanup(&self, lifetime: Duration) -> Result<()> {
#[cfg(feature = "kv-tikv")]
if let Some(ops) = self.tikv_ops() {
return ops.run_lock_cleanup(lifetime).await.map_err(Into::into);
}
let _ = lifetime;
Ok(())
}
pub fn in_flight_transaction_count(&self) -> Option<usize> {
#[cfg(feature = "kv-tikv")]
if let Some(ops) = self.tikv_ops() {
return Some(ops.in_flight_transaction_count());
}
None
}
#[cfg(feature = "kv-tikv")]
fn tikv_ops(&self) -> Option<Arc<super::tikv::TikvOpsHandle>> {
let ext = self
.transaction_factory
.builder
.extension(TypeId::of::<super::tikv::TikvOpsHandle>())?;
ext.downcast::<super::tikv::TikvOpsHandle>().ok()
}
#[cfg(feature = "surrealism")]
pub async fn eager_load_surrealism_modules(&self) {
use crate::catalog::providers::{DatabaseProvider, NamespaceProvider};
use crate::surrealism::cache::SurrealismCacheLookup;
let txn = match self.transaction(Read, Optimistic).await {
Ok(txn) => Arc::new(txn),
Err(e) => {
warn!(target: TARGET, error = %e, "Surrealism eager load: failed to open transaction");
return;
}
};
let mut ctx = match self.setup_ctx() {
Ok(ctx) => ctx,
Err(e) => {
warn!(target: TARGET, error = %e, "Surrealism eager load: failed to set up context");
return;
}
};
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
let nss = match txn.all_ns(None).await {
Ok(nss) => nss,
Err(e) => {
warn!(target: TARGET, error = %e, "Surrealism eager load: failed to list namespaces");
return;
}
};
struct ModuleLookup {
ns_id: crate::catalog::NamespaceId,
db_id: crate::catalog::DatabaseId,
bucket: String,
key: String,
display_name: String,
}
let mut lookups = Vec::new();
for ns in nss.iter() {
let dbs = match txn.all_db(ns.namespace_id, None).await {
Ok(dbs) => dbs,
Err(e) => {
warn!(
target: TARGET,
error = %e, ns = %ns.name,
"Surrealism eager load: failed to list databases"
);
continue;
}
};
for db in dbs.iter() {
let modules = match txn.all_db_modules(ns.namespace_id, db.database_id, None).await
{
Ok(m) => m,
Err(e) => {
warn!(
target: TARGET,
error = %e, ns = %ns.name, db = %db.name,
"Surrealism eager load: failed to list modules"
);
continue;
}
};
for md in modules.iter() {
if let crate::catalog::ModuleExecutable::Surrealism(s) = &md.executable {
lookups.push(ModuleLookup {
ns_id: ns.namespace_id,
db_id: db.database_id,
bucket: s.bucket.clone(),
key: s.key.clone(),
display_name: md
.name
.clone()
.unwrap_or_else(|| "<unnamed>".to_string()),
});
}
}
}
}
if lookups.is_empty() {
debug!(target: TARGET, "Surrealism eager load: no modules to load");
return;
}
let total = lookups.len();
debug!(target: TARGET, count = total, "Surrealism eager load: loading modules");
let concurrency =
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(8).clamp(2, 16);
let load_sem = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
let mut join_set = tokio::task::JoinSet::new();
for lookup in lookups {
let ctx = Arc::clone(&ctx);
let load_sem = Arc::clone(&load_sem);
join_set.spawn(async move {
let _permit = load_sem
.acquire_owned()
.await
.expect("Surrealism eager load semaphore must not be closed");
let cache_lookup = SurrealismCacheLookup::File(
&lookup.ns_id,
&lookup.db_id,
&lookup.bucket,
&lookup.key,
);
match ctx.get_surrealism_runtime(cache_lookup).await {
Ok(_) => {
debug!(
target: TARGET,
module = %lookup.display_name,
"Surrealism eager load: loaded module"
);
true
}
Err(e) => {
warn!(
target: TARGET,
module = %lookup.display_name,
error = %e,
"Surrealism eager load: failed to load module"
);
false
}
}
});
}
let mut loaded = 0usize;
let mut failed = 0usize;
while let Some(result) = join_set.join_next().await {
match result {
Ok(true) => loaded += 1,
Ok(false) => failed += 1,
Err(e) => {
warn!(target: TARGET, error = %e, "Surrealism eager load: task panicked");
failed += 1;
}
}
}
if failed > 0 {
warn!(
target: TARGET,
loaded, failed, total,
"Surrealism eager load: completed with failures"
);
} else {
tracing::info!(
target: TARGET,
loaded, total,
"Surrealism eager load: all modules loaded"
);
}
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn bootstrap(&self) -> Result<()> {
Self::retry("Insert node", || self.insert_node()).await?;
Self::retry("Expire nodes", || self.expire_nodes()).await?;
Self::retry("Remove nodes", || self.remove_nodes()).await?;
Ok(())
}
async fn retry<F, Fut, R>(task: &str, func: F) -> Result<R>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<R>>,
{
let global_timeout = Duration::from_secs(120);
let per_attempt_timeout = Duration::from_secs(10);
let time = Instant::now();
let mut last_error = None;
let mut attempt = 1;
loop {
let remaining = global_timeout.saturating_sub(time.elapsed());
if remaining.is_zero() {
break;
}
let attempt_timeout = (per_attempt_timeout * attempt).min(remaining);
if let Ok(result) = timeout(attempt_timeout, func()).await {
match result {
Ok(result) => return Ok(result),
Err(e) => {
if let Some(crate::kvs::Error::TransactionConflict(_)) = e.downcast_ref() {
last_error = Some(e);
} else {
return Err(e);
}
}
}
}
if time.elapsed() >= global_timeout {
break;
}
let remaining = global_timeout.saturating_sub(time.elapsed());
if remaining.is_zero() {
break;
}
let tempo = Duration::from_secs(rand::rng().random_range(0..10)).min(remaining);
sleep(tempo).await;
attempt += 1;
}
if let Some(e) = last_error {
error!(target: TARGET, "{task} - All {attempt} attempts failed. Last error: {e}");
} else {
error!(target: TARGET, "{task} - All {attempt} attempts failed.");
}
bail!(Error::Internal(format!("{task} failed after {attempt} attempts due to timeout")));
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn insert_node(&self) -> Result<()> {
trace!(target: TARGET, id = %self.id,"Inserting node in the cluster");
crate::sys::refresh().await;
let txn = self.transaction(Write, Optimistic).await?;
let key = crate::key::root::nd::Nd::new(self.id);
let now = self.clock_now();
let node = Node::new_with_endpoint(self.id, now, false, self.http_endpoint.clone());
run!(txn, txn.set(&key, &node).await)
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn update_node(&self) -> Result<()> {
trace!(target: TARGET, id = %self.id, "Updating node in the cluster");
crate::sys::refresh().await;
let txn = self.transaction(Write, Optimistic).await?;
let key = crate::key::root::nd::new(self.id);
let now = self.clock_now();
let node = Node::new_with_endpoint(self.id, now, false, self.http_endpoint.clone());
run!(txn, txn.replace(&key, &node).await)
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self, canceller))]
pub async fn update_node_with_timeout(
&self,
timeout_duration: Duration,
canceller: &CancellationToken,
) -> Result<()> {
trace!(target: TARGET, id = %self.id, timeout = ?timeout_duration, "Updating node in the cluster with timeout");
let deadline = Instant::now() + timeout_duration;
await_node_step(deadline, timeout_duration, Some(canceller), async {
crate::sys::refresh().await;
Ok(())
})
.await?;
let txn = await_node_step(
deadline,
timeout_duration,
Some(canceller),
self.transaction(Write, Optimistic),
)
.await?;
let key = crate::key::root::nd::new(self.id);
let now = self.clock_now();
let node = Node::new_with_endpoint(self.id, now, false, self.http_endpoint.clone());
await_node_tx_step(
&txn,
deadline,
timeout_duration,
Some(canceller),
txn.replace(&key, &node),
)
.await?;
await_node_tx_step(&txn, deadline, timeout_duration, Some(canceller), txn.commit()).await
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn delete_node(&self) -> Result<()> {
trace!(target: TARGET, id = %self.id, "Archiving node in the cluster");
let txn = self.transaction(Write, Optimistic).await?;
let key = crate::key::root::nd::new(self.id);
let val = catch!(txn, txn.get_node(self.id).await);
let node = val.as_ref().archive();
run!(txn, txn.replace(&key, &node).await)
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn delete_node_with_timeout(&self, timeout_duration: Duration) -> Result<()> {
trace!(target: TARGET, id = %self.id, timeout = ?timeout_duration, "Archiving node in the cluster with timeout");
let deadline = Instant::now() + timeout_duration;
let txn =
await_node_step(deadline, timeout_duration, None, self.transaction(Write, Optimistic))
.await?;
let key = crate::key::root::nd::new(self.id);
let val = await_node_tx_step(&txn, deadline, timeout_duration, None, txn.get_node(self.id))
.await?;
let node = val.as_ref().archive();
await_node_tx_step(&txn, deadline, timeout_duration, None, txn.replace(&key, &node))
.await?;
await_node_tx_step(&txn, deadline, timeout_duration, None, txn.commit()).await
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn expire_nodes(&self) -> Result<()> {
trace!(target: TARGET, "Archiving expired nodes in the cluster");
let inactive = {
let txn = self.transaction(Read, Optimistic).await?;
let nds = catch!(txn, txn.all_nodes().await);
let now = self.clock_now();
catch!(txn, txn.cancel().await);
nds.iter()
.filter_map(|n| {
match n.is_active() && n.heartbeat < now - Duration::from_secs(30) {
true => Some(n.to_owned()),
false => None,
}
})
.collect::<Vec<_>>()
};
if !inactive.is_empty() {
let txn = self.transaction(Write, Optimistic).await?;
for nd in inactive.iter() {
trace!(target: TARGET, id = %nd.id, "Archiving node in the cluster");
let node = nd.archive();
let key = crate::key::root::nd::new(nd.id);
catch!(txn, txn.replace(&key, &node).await);
}
catch!(txn, txn.commit().await);
}
Ok(())
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn remove_nodes(&self) -> Result<()> {
trace!(target: TARGET, "Cleaning up archived nodes in the cluster");
let archived = {
let txn = self.transaction(Read, Optimistic).await?;
let nds = catch!(txn, txn.all_nodes().await);
catch!(txn, txn.cancel().await);
nds.iter().filter_map(Node::archived).collect::<Vec<_>>()
};
for id in archived.iter() {
let beg = crate::key::node::lq::prefix(*id)?;
let end = crate::key::node::lq::suffix(*id)?;
let mut next = Some(beg..end);
let txn = self.transaction(Write, Optimistic).await?;
{
trace!(target: TARGET, id = %id, "Deleting live queries for node");
while let Some(rng) = next {
let res = catch!(txn, txn.batch_keys_vals(rng, NORMAL_BATCH_SIZE, None).await);
next = res.next;
for (k, v) in res.result.iter() {
let val: NodeLiveQuery = KVValue::kv_decode_value(v, ())?;
let nlq = catch!(txn, crate::key::node::lq::Lq::decode_key(k));
if archived.contains(&nlq.nd) {
let tlq = crate::key::table::lq::new(val.ns, val.db, &val.tb, nlq.lq);
catch!(txn, txn.clr(&tlq).await);
catch!(txn, txn.clr(&nlq).await);
}
}
yield_now!();
}
}
{
trace!(target: TARGET, id = %id, "Deleting node from the cluster");
let key = crate::key::root::nd::new(*id);
catch!(txn, txn.clr(&key).await);
}
catch!(txn, txn.commit().await);
}
Ok(())
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self, session))]
pub async fn persist_rpc_session(
&self,
id: Uuid,
session: &Session,
ttl: Duration,
) -> Result<()> {
trace!(target: TARGET, id = %id, "Persisting durable RPC session");
let expires_at = self.clock_now().value + ttl.as_millis() as u64;
let value = DurableSession::from_session(session, expires_at);
let key = crate::key::root::se::Se::new(id);
let txn = self.transaction(Write, Optimistic).await?;
run!(txn, txn.set(&key, &value).await)
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self, session))]
pub async fn create_rpc_session(
&self,
id: Uuid,
session: &Session,
ttl: Duration,
) -> Result<bool> {
trace!(target: TARGET, id = %id, "Creating durable RPC session");
let key = crate::key::root::se::Se::new(id);
let expires_at = self.clock_now().value + ttl.as_millis() as u64;
let value = DurableSession::from_session(session, expires_at);
let txn = self.transaction(Write, Optimistic).await?;
match txn.putc(&key, &value, None).await {
Ok(()) => match txn.commit().await {
Ok(()) => Ok(true),
Err(e) if is_conditional_write_conflict(&e) => {
let _ = txn.cancel().await;
Ok(false)
}
Err(e) => {
let _ = txn.cancel().await;
Err(e)
}
},
Err(e) => {
let _ = txn.cancel().await;
if is_conditional_write_conflict(&e) {
Ok(false)
} else {
Err(e)
}
}
}
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self, session))]
pub async fn update_rpc_session(
&self,
id: Uuid,
session: &Session,
ttl: Duration,
) -> Result<bool> {
trace!(target: TARGET, id = %id, "Updating durable RPC session");
let key = crate::key::root::se::Se::new(id);
let txn = self.transaction(Write, Optimistic).await?;
let Some(current) = catch!(txn, txn.get(&key, None).await) else {
let _ = txn.cancel().await;
return Ok(false);
};
let expires_at = self.clock_now().value + ttl.as_millis() as u64;
let value = DurableSession::from_session(session, expires_at);
match txn.putc(&key, &value, Some(¤t)).await {
Ok(()) => match txn.commit().await {
Ok(()) => Ok(true),
Err(e) if is_conditional_write_conflict(&e) => {
let _ = txn.cancel().await;
Ok(false)
}
Err(e) => {
let _ = txn.cancel().await;
Err(e)
}
},
Err(e) => {
let _ = txn.cancel().await;
if is_conditional_write_conflict(&e) {
Ok(false)
} else {
Err(e)
}
}
}
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn load_rpc_session(&self, id: Uuid) -> Result<Option<(Session, u64)>> {
trace!(target: TARGET, id = %id, "Loading durable RPC session");
let key = crate::key::root::se::Se::new(id);
let now = self.clock_now().value;
let durable = {
let txn = self.transaction(Read, Optimistic).await?;
let val = catch!(txn, txn.get(&key, None).await);
catch!(txn, txn.cancel().await);
val
};
let Some(durable) = durable else {
return Ok(None);
};
if durable.expires_at <= now {
trace!(target: TARGET, id = %id, "Durable RPC session has expired");
let txn = self.transaction(Write, Optimistic).await?;
match txn.delc(&key, Some(&durable)).await {
Ok(()) => match txn.commit().await {
Ok(()) => {}
Err(e) if is_conditional_write_conflict(&e) => {
let _ = txn.cancel().await;
}
Err(e) => {
let _ = txn.cancel().await;
return Err(e);
}
},
Err(e) => {
let _ = txn.cancel().await;
if !is_conditional_write_conflict(&e) {
return Err(e);
}
}
}
return Ok(None);
}
let expires_at = durable.expires_at;
durable.into_session().map(|session| Some((session, expires_at)))
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn delete_rpc_session(&self, id: Uuid) -> Result<()> {
trace!(target: TARGET, id = %id, "Deleting durable RPC session");
let key = crate::key::root::se::Se::new(id);
let txn = self.transaction(Write, Optimistic).await?;
let Some(current) = catch!(txn, txn.get(&key, None).await) else {
let _ = txn.cancel().await;
return Ok(());
};
match txn.delc(&key, Some(¤t)).await {
Ok(()) => match txn.commit().await {
Ok(()) => Ok(()),
Err(e) => {
let _ = txn.cancel().await;
Err(e)
}
},
Err(e) => {
let _ = txn.cancel().await;
Err(e)
}
}
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn purge_expired_rpc_sessions(&self, interval: &Duration) -> Result<()> {
trace!(target: TARGET, "Attempting expired RPC session purge");
let lh = LeaseHandler::new(
self.sequences.clone(),
self.id,
self.transaction_factory.clone(),
TaskLeaseType::RpcSessionCleanup,
*interval * 2,
)?;
if !lh.has_lease().await? {
return Ok(());
}
trace!(target: TARGET, "Purging expired RPC sessions");
let now = self.clock_now().value;
let mut next = Some(crate::key::root::se::SePrefix {}.encode_range()?);
while let Some(rng) = next {
let batch = {
let txn = self.transaction(Read, Optimistic).await?;
let res = catch!(txn, txn.batch_keys_vals(rng, NORMAL_BATCH_SIZE, None).await);
catch!(txn, txn.cancel().await);
res
};
next = batch.next;
for (k, v) in batch.result.iter() {
let val: DurableSession = match KVValue::kv_decode_value(v, ()) {
Ok(val) => val,
Err(e) => {
warn!(target: TARGET, "Skipping undecodable durable RPC session entry: {e}");
continue;
}
};
if val.expires_at <= now {
let key = crate::key::root::se::Se::decode_key(k)?;
trace!(target: TARGET, id = %key.id, "Purging expired RPC session");
let txn = self.transaction(Write, Optimistic).await?;
match txn.delc(&key, Some(&val)).await {
Ok(()) => match txn.commit().await {
Ok(()) => {}
Err(e) if is_conditional_write_conflict(&e) => {
let _ = txn.cancel().await;
}
Err(e) => {
let _ = txn.cancel().await;
return Err(e);
}
},
Err(e) => {
let _ = txn.cancel().await;
if !is_conditional_write_conflict(&e) {
return Err(e);
}
}
}
}
}
yield_now!();
}
Ok(())
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn garbage_collect(&self) -> Result<()> {
trace!(target: TARGET, "Garbage collecting all miscellaneous data");
let archived = {
let txn = self.transaction(Read, Optimistic).await?;
let nds = catch!(txn, txn.all_nodes().await);
txn.cancel().await?;
nds.iter().filter_map(Node::archived).collect::<Vec<_>>()
};
let nss = {
let txn = self.transaction(Read, Optimistic).await?;
let res = catch!(txn, txn.all_ns(None).await);
txn.cancel().await?;
res
};
for ns in nss.iter() {
trace!(target: TARGET, "Garbage collecting data in namespace {}", ns.name);
let dbs = {
let txn = self.transaction(Read, Optimistic).await?;
let res = catch!(txn, txn.all_db(ns.namespace_id, None).await);
txn.cancel().await?;
res
};
for db in dbs.iter() {
trace!(target: TARGET, "Garbage collecting data in database {}/{}", ns.name, db.name);
let tbs = {
let txn = self.transaction(Read, Optimistic).await?;
let res = catch!(txn, txn.all_tb(ns.namespace_id, db.database_id, None).await);
txn.cancel().await?;
res
};
for tb in tbs.iter() {
trace!(target: TARGET, "Garbage collecting data in table {}/{}/{}", ns.name, db.name, tb.name);
let beg =
crate::key::table::lq::prefix(db.namespace_id, db.database_id, &tb.name)?;
let end =
crate::key::table::lq::suffix(db.namespace_id, db.database_id, &tb.name)?;
let mut next = Some(beg..end);
let txn = self.transaction(Write, Optimistic).await?;
while let Some(rng) = next {
let max = NORMAL_BATCH_SIZE;
let res = catch!(txn, txn.batch_keys_vals(rng, max, None).await);
next = res.next;
for (k, v) in res.result.iter() {
let stm: SubscriptionDefinition = KVValue::kv_decode_value(v, ())?;
let (nid, lid) = (stm.node, stm.id);
if archived.contains(&stm.node) {
let tlq = catch!(txn, crate::key::table::lq::Lq::decode_key(k));
let nlq = crate::key::node::lq::new(nid, lid);
catch!(txn, txn.clr(&nlq).await);
catch!(txn, txn.clr(&tlq).await);
}
}
yield_now!();
}
catch!(txn, txn.commit().await);
}
}
}
Ok(())
}
#[instrument(err, level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn delete_queries(&self, ids: Vec<uuid::Uuid>) -> Result<()> {
trace!(target: TARGET, "Deleting live queries for a connection");
let txn = self.transaction(Write, Optimistic).await?;
for id in ids {
let nlq = crate::key::node::lq::new(self.id(), id);
if let Some(lq) = catch!(txn, txn.get(&nlq, None).await) {
let nlq = crate::key::node::lq::new(self.id(), id);
let tlq = crate::key::table::lq::new(lq.ns, lq.db, &lq.tb, id);
catch!(txn, txn.clr(&tlq).await);
catch!(txn, txn.clr(&nlq).await);
}
}
catch!(txn, txn.commit().await);
Ok(())
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn changefeed_process(&self, interval: &Duration) -> Result<()> {
trace!(target: TARGET, "Attempting changefeed garbage collection");
let lh = LeaseHandler::new(
self.sequences.clone(),
self.id,
self.transaction_factory.clone(),
TaskLeaseType::ChangeFeedCleanup,
*interval * 2,
)?;
if !lh.has_lease().await? {
return Ok(());
}
trace!(target: TARGET, "Running changefeed garbage collection");
let txn = self.transaction(Write, Optimistic).await?;
catch!(txn, crate::cf::gc_all_at(&lh, &txn).await);
if self.config.live_query_engine == LiveQueryEngine::Router {
catch!(
txn,
crate::lq::gc::gc_all_at(&lh, &txn, self.config.live_query_retention).await
);
}
catch!(txn, txn.commit().await);
Ok(())
}
#[instrument(level = "trace", target = "surrealdb::core::lq", skip(self))]
pub async fn live_query_router_process(&self) -> Result<()> {
if self.config.live_query_engine != LiveQueryEngine::Router {
return Ok(());
}
crate::lq::router::process(self, &self.live_query_router).await
}
fn ensure_not_cancelled(canceller: &CancellationToken) -> Result<()> {
if canceller.is_cancelled() {
bail!(Error::QueryCancelled);
}
Ok(())
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(self, canceller))]
pub async fn resume_stalled_index_builds(
&self,
interval: Duration,
canceller: CancellationToken,
) -> Result<usize> {
Self::ensure_not_cancelled(&canceller)?;
let lh = LeaseHandler::new_with_canceller(
self.sequences.clone(),
self.id,
self.transaction_factory.clone(),
TaskLeaseType::IndexBuildResume,
interval * 2,
canceller.clone(),
)?;
if !lh.has_lease().await? {
return Ok(0);
}
if self.index_builder.has_unfinished_build().await {
trace!(
target: TARGET,
"Deferring stalled index build adoption; a local index build is still running"
);
return Ok(0);
}
let mut candidates = Vec::new();
{
let txn = self.transaction(Read, Optimistic).await?;
let res: Result<()> = async {
for ns in txn.all_ns(None).await?.iter() {
for db in txn.all_db(ns.namespace_id, None).await?.iter() {
for tb in txn.all_tb(ns.namespace_id, db.database_id, None).await?.iter() {
for ix in txn
.all_tb_indexes(ns.namespace_id, db.database_id, &tb.name, None)
.await?
.iter()
{
if ix.prepare_remove {
continue;
}
candidates.push((
ns.namespace_id,
ns.name.clone(),
db.database_id,
db.name.clone(),
tb.table_id,
Arc::new(ix.clone()),
));
}
}
}
}
Ok(())
}
.await;
let _ = txn.cancel().await;
res?;
}
let index_builder = &self.index_builder;
let mut resumed = 0;
for (ns_id, ns_name, db_id, db_name, tb_id, ix) in candidates {
Self::ensure_not_cancelled(&canceller)?;
lh.try_maintain_lease().await?;
let ctx = self.setup_ctx()?.freeze();
let opt = self.setup_options(
&Session::owner().with_ns(ns_name.as_str()).with_db(db_name.as_str()),
);
match index_builder
.resume_stalled(&ctx, opt, ns_id, db_id, tb_id, Arc::clone(&ix))
.await
{
Ok(true) => {
resumed += 1;
info!(
target: TARGET,
"Resuming stalled index build '{}' on table '{}'",
ix.name, ix.table_name
);
break;
}
Ok(false) => {}
Err(e) => {
warn!(
target: TARGET,
"Failed to resume stalled index build '{}' on table '{}': {e}",
ix.name, ix.table_name
);
}
}
}
Ok(resumed)
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(dbs, canceller))]
pub async fn index_compaction(
dbs: Arc<Datastore>,
interval: Duration,
canceller: CancellationToken,
) -> Result<(usize, usize)> {
trace!(target: TARGET, "Attempting index compaction process");
let lh = LeaseHandler::new_with_canceller(
dbs.sequences.clone(),
dbs.id,
dbs.transaction_factory.clone(),
TaskLeaseType::IndexCompaction,
interval * 2,
canceller.clone(),
)?;
let mut count_iteration = 0;
let mut count_error = 0;
'compaction: loop {
Self::ensure_not_cancelled(&canceller)?;
if !lh.has_lease().await? {
return Ok((count_iteration, count_error));
}
Self::ensure_not_cancelled(&canceller)?;
trace!(target: TARGET, "Running index compaction process");
let (beg, end) = IndexCompactionKey::range();
let range = beg..end;
let items = {
let txn = dbs.transaction(Read, Optimistic).await?;
let res = txn.getr(range, None).await;
let _ = txn.cancel().await;
res?
};
Self::ensure_not_cancelled(&canceller)?;
if items.is_empty() {
return Ok((count_iteration, count_error));
}
let keys: Vec<Key> = items.iter().map(|(k, _)| k.clone()).collect();
count_iteration += 1;
count_error +=
Self::index_compaction_loop(Arc::clone(&dbs), &lh, items, canceller.clone())
.await?;
loop {
let txn = dbs.transaction(Write, Optimistic).await?;
if let Err(e) = Self::ensure_not_cancelled(&canceller) {
let _ = txn.cancel().await;
return Err(e);
}
for k in &keys {
if let Err(e) = txn.del(k).await {
warn!(target: TARGET, "Failed to delete compaction queue entry: {e}");
}
}
if let Err(e) = Self::ensure_not_cancelled(&canceller) {
let _ = txn.cancel().await;
return Err(e);
}
#[cfg(test)]
if let Err(e) = maybe_inject_retryable_conflict(
RetryableConflictSite::IndexCompactionQueueCleanup,
dbs.id,
) {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
"Retryable conflict committing compaction queue cleanup, retrying",
)
.await
{
continue;
}
warn!(target: TARGET, "Failed to commit compaction queue cleanup: {e}");
break 'compaction;
}
if let Err(e) = txn.commit().await {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
"Retryable conflict committing compaction queue cleanup, retrying",
)
.await
{
continue;
}
warn!(target: TARGET, "Failed to commit compaction queue cleanup: {e}");
break 'compaction;
}
break;
}
}
Ok((count_iteration, count_error))
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(dbs, canceller))]
pub async fn reclaim_tombstones(
dbs: Arc<Datastore>,
interval: Duration,
grace: Duration,
canceller: CancellationToken,
) -> Result<(usize, usize)> {
trace!(target: TARGET, "Attempting tombstone reclaim process");
let lh = LeaseHandler::new_with_canceller(
dbs.sequences.clone(),
dbs.id,
dbs.transaction_factory.clone(),
TaskLeaseType::ReclaimTombstones,
interval * 2,
canceller.clone(),
)?;
let mut count_iteration = 0;
let mut count_error = 0;
loop {
Self::ensure_not_cancelled(&canceller)?;
if !lh.has_lease().await? {
return Ok((count_iteration, count_error));
}
Self::ensure_not_cancelled(&canceller)?;
let (beg, end) = ReclaimKey::range();
let items = {
let txn = dbs.transaction(Read, Optimistic).await?;
let res = txn.getr(beg..end, None).await;
let _ = txn.cancel().await;
res?
};
Self::ensure_not_cancelled(&canceller)?;
if items.is_empty() {
return Ok((count_iteration, count_error));
}
count_iteration += 1;
let now_ms = web_time::SystemTime::now()
.duration_since(web_time::SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let grace_ms = grace.as_millis() as u64;
let mut done: Vec<Key> = Vec::with_capacity(items.len());
let mut to_stamp: Vec<Key> = Vec::new();
for (k, v) in &items {
Self::ensure_not_cancelled(&canceller)?;
lh.try_maintain_lease().await?;
let rc = match ReclaimKey::decode_key(k) {
Ok(rc) => rc,
Err(e) => {
count_error += 1;
warn!(target: TARGET, "Failed to decode reclaim queue entry: {e}");
continue;
}
};
if grace.is_zero() {
match dbs.reclaim_decoded(&rc).await {
Ok(()) => done.push(k.clone()),
Err(e) => {
count_error += 1;
warn!(target: TARGET, "Tombstone reclaim failed for a queue entry: {e}");
}
}
continue;
}
let state = match ReclaimState::kv_decode_value(v, ()) {
Ok(s) => s,
Err(e) => {
count_error += 1;
warn!(target: TARGET, "Failed to decode reclaim queue state: {e}");
continue;
}
};
if state.observed_ms == 0 {
to_stamp.push(k.clone());
continue;
}
if now_ms.saturating_sub(state.observed_ms) >= grace_ms {
match dbs.reclaim_decoded(&rc).await {
Ok(()) => done.push(k.clone()),
Err(e) => {
count_error += 1;
warn!(target: TARGET, "Tombstone reclaim failed for a queue entry: {e}");
}
}
}
}
if !done.is_empty() || !to_stamp.is_empty() {
let txn = dbs.transaction(Write, Optimistic).await?;
if let Err(e) = Self::ensure_not_cancelled(&canceller) {
let _ = txn.cancel().await;
return Err(e);
}
for k in &to_stamp {
if let Ok(rc) = ReclaimKey::decode_key(k) {
let state = ReclaimState {
observed_ms: now_ms,
};
if let Err(e) = txn.set(&rc, &state).await {
warn!(target: TARGET, "Failed to stamp reclaim queue entry: {e}");
}
}
}
for k in &done {
if let Err(e) = txn.del(k).await {
warn!(target: TARGET, "Failed to delete reclaim queue entry: {e}");
}
}
if let Err(e) = txn.commit().await {
let _ = txn.cancel().await;
warn!(target: TARGET, "Failed to commit reclaim queue update: {e}");
return Ok((count_iteration, count_error));
}
}
if done.is_empty() {
return Ok((count_iteration, count_error));
}
}
}
async fn reclaim_decoded(&self, rc: &ReclaimKey<'_>) -> Result<()> {
let expunge = rc.expunge != 0;
match rc.kind {
RECLAIM_NAMESPACE => {
let prefix = crate::key::namespace::all::new(rc.ns);
self.reclaim_prefix(&prefix, expunge).await
}
RECLAIM_DATABASE => {
let prefix = crate::key::database::all::new(rc.ns, rc.db);
self.reclaim_prefix(&prefix, expunge).await
}
RECLAIM_INDEX => {
let prefix = crate::key::index::all::new(rc.ns, rc.db, rc.tb.as_ref(), rc.ix);
self.reclaim_prefix(&prefix, expunge).await
}
other => {
warn!(target: TARGET, "Unknown reclaim queue entry kind {other}, skipping");
Ok(())
}
}
}
async fn reclaim_prefix<K>(&self, prefix: &K, expunge: bool) -> Result<()>
where
K: crate::kvs::KVKey + std::fmt::Debug,
{
#[cfg(feature = "kv-tikv")]
if self.tikv_ops().is_some() {
let range = crate::kvs::util::to_prefix_range(prefix)?;
return self.unsafe_destroy_range(range.start, range.end).await;
}
let txn = self.transaction(Write, Optimistic).await?;
let res = if expunge {
txn.clrp(prefix).await
} else {
txn.delp(prefix).await
};
match res {
Ok(()) => txn.commit().await,
Err(e) => {
let _ = txn.cancel().await;
Err(e)
}
}
}
#[cfg(not(target_family = "wasm"))]
async fn await_index_compaction_handle(
ikb: &IndexKeyBase,
handle: &mut tokio::task::JoinHandle<Result<()>>,
canceller: &CancellationToken,
) {
match handle.await {
Ok(Ok(())) => {}
Ok(Err(e))
if canceller.is_cancelled()
&& matches!(e.downcast_ref::<Error>(), Some(Error::QueryCancelled)) => {}
Ok(Err(e)) => {
warn!("Index compaction {ikb} fails while awaiting cancellation: {e}");
}
Err(e) => {
warn!("Index compaction {ikb} join fails while awaiting cancellation: {e}");
}
}
}
#[cfg(not(target_family = "wasm"))]
async fn await_index_compaction_handles(
handles: &mut Vec<(IndexKeyBase, tokio::task::JoinHandle<Result<()>>)>,
canceller: &CancellationToken,
) {
while let Some((ikb, mut handle)) = handles.pop() {
Self::await_index_compaction_handle(&ikb, &mut handle, canceller).await;
}
}
#[cfg(not(target_family = "wasm"))]
async fn index_compaction_loop(
dbs: Arc<Datastore>,
lh: &LeaseHandler,
items: Vec<(Key, Val)>,
canceller: CancellationToken,
) -> Result<usize> {
let mut compacted_indexes = HashMap::new();
for (k, _) in items {
Self::ensure_not_cancelled(&canceller)?;
lh.try_maintain_lease().await?;
let ic = IndexCompactionKey::decode_key(&k)?;
let ikb = IndexKeyBase::new(ic.ns, ic.db, ic.tb.as_ref().clone(), ic.ix);
if let Entry::Vacant(e) = compacted_indexes.entry(ikb) {
e.insert(());
}
}
let mut error_count = 0;
let mut handles: Vec<(IndexKeyBase, tokio::task::JoinHandle<Result<()>>)> =
Vec::with_capacity(compacted_indexes.len());
for (ikb, _) in compacted_indexes {
if let Err(e) = Self::ensure_not_cancelled(&canceller) {
Self::await_index_compaction_handles(&mut handles, &canceller).await;
return Err(e);
}
let dbs = Arc::clone(&dbs);
let canceller = canceller.clone();
let task_ikb = ikb.clone();
let jh = spawn(async move { dbs.process_index_compaction(&task_ikb, canceller).await });
handles.push((ikb, jh));
}
while let Some((ikb, mut jh)) = handles.pop() {
let res = tokio::select! {
biased;
_ = canceller.cancelled() => {
Self::await_index_compaction_handle(&ikb, &mut jh, &canceller).await;
Self::await_index_compaction_handles(&mut handles, &canceller).await;
bail!(Error::QueryCancelled);
}
res = &mut jh => res?,
};
if let Err(e) = res {
if canceller.is_cancelled() {
Self::await_index_compaction_handles(&mut handles, &canceller).await;
return Err(e);
}
error_count += 1;
warn!("Index compaction {ikb} fails: {e}");
}
}
Ok(error_count)
}
#[cfg(target_family = "wasm")]
async fn index_compaction_loop(
dbs: Arc<Datastore>,
lh: &LeaseHandler,
items: Vec<(Key, Val)>,
canceller: CancellationToken,
) -> Result<usize> {
let mut seen = HashSet::new();
let mut error_count = 0;
for (k, _) in items {
Self::ensure_not_cancelled(&canceller)?;
lh.try_maintain_lease().await?;
let ic = IndexCompactionKey::decode_key(&k)?;
let ikb = IndexKeyBase::new(ic.ns, ic.db, ic.tb.as_ref().clone(), ic.ix);
if !seen.insert(ikb.clone()) {
continue;
}
let res: Result<()> =
async { dbs.process_index_compaction(&ikb, canceller.clone()).await }.await;
if let Err(e) = res {
if canceller.is_cancelled() {
return Err(e);
}
error_count += 1;
warn!("Index compaction {ikb} fails: {e}");
}
}
Ok(error_count)
}
async fn process_index_compaction(
&self,
ikb: &IndexKeyBase,
canceller: CancellationToken,
) -> Result<()> {
Self::ensure_not_cancelled(&canceller)?;
let ix = {
let txn = self.transaction(Read, Optimistic).await?;
let res =
txn.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None).await;
let _ = txn.cancel().await;
res?
};
Self::ensure_not_cancelled(&canceller)?;
match ix {
Some(ix) if !ix.prepare_remove => match &ix.index {
Index::FullText(p) => {
self.process_fulltext_compaction(ikb, p, &canceller).await?;
}
Index::Count(_) => {
self.process_count_compaction(ikb, &canceller).await?;
}
Index::Hnsw(_) => {
self.process_hnsw_compaction(ikb, &canceller).await?;
}
#[cfg(diskann)]
Index::DiskAnn(_) => {
self.process_diskann_compaction(ikb, &canceller).await?;
}
_ => {
trace!(target: TARGET, "Index compaction: Index {:?} does not support compaction, skipping", ikb);
}
},
_ => {
trace!(target: TARGET, "Index compaction: Index {:?} not found, skipping", ikb);
}
}
Ok(())
}
async fn process_hnsw_compaction(
&self,
ikb: &IndexKeyBase,
canceller: &CancellationToken,
) -> Result<()> {
loop {
Self::ensure_not_cancelled(canceller)?;
let prepared = {
let txn = Arc::new(self.transaction(Read, Optimistic).await?);
let res: Result<
Option<(
crate::catalog::TableId,
crate::idx::trees::hnsw::index::HnswCompactionPlan,
)>,
> = async {
let Some(tb) = txn.get_tb(ikb.ns(), ikb.db(), ikb.table(), None).await? else {
return Ok(None);
};
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix) if !ix.prepare_remove && matches!(&ix.index, Index::Hnsw(_)) => {
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
let plan = IndexOperation::prepare_hnsw_compaction(&ctx, ikb).await?;
Ok(Some((tb.table_id, plan)))
}
_ => Ok(None),
}
}
.await;
let _ = txn.cancel().await;
res?
};
let Some((tb, plan)) = prepared else {
return Ok(());
};
if !plan.has_work() {
return Ok(());
}
let has_more = plan.has_more();
Self::ensure_not_cancelled(canceller)?;
let txn = Arc::new(self.transaction(Write, Optimistic).await?);
let res: Result<bool> = async {
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix) if !ix.prepare_remove => match &ix.index {
Index::Hnsw(p) => {
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
IndexOperation::apply_hnsw_compaction(
&ctx,
&self.index_stores,
ikb,
&ix,
p,
plan,
)
.await
}
_ => Ok(false),
},
_ => Ok(false),
}
}
.await;
match res {
Ok(true) => {
if let Err(e) = Self::ensure_not_cancelled(canceller) {
let _ = txn.cancel().await;
if let Err(evict) =
self.index_stores.remove_hnsw_index(tb, ikb.clone()).await
{
warn!(target: TARGET, "Failed to evict HNSW index after compaction cancellation: {evict}");
}
return Err(e);
}
#[cfg(test)]
if let Err(e) = maybe_inject_retryable_conflict(
RetryableConflictSite::HnswCompaction,
self.id,
) {
let _ = txn.cancel().await;
if let Err(evict) =
self.index_stores.remove_hnsw_index(tb, ikb.clone()).await
{
warn!(target: TARGET, "Failed to evict HNSW index after compaction commit error: {evict}");
}
if Self::retry_index_operation_conflict(
&e,
format!(
"Retryable conflict committing HNSW compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
if let Err(e) = txn.commit().await {
let _ = txn.cancel().await;
if let Err(evict) =
self.index_stores.remove_hnsw_index(tb, ikb.clone()).await
{
warn!(target: TARGET, "Failed to evict HNSW index after compaction commit error: {evict}");
}
if Self::retry_index_operation_conflict(
&e,
format!(
"Retryable conflict committing HNSW compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
}
Ok(false) => {
let _ = txn.cancel().await;
return Ok(());
}
Err(e) => {
let _ = txn.cancel().await;
if let Err(evict) = self.index_stores.remove_hnsw_index(tb, ikb.clone()).await {
warn!(target: TARGET, "Failed to evict HNSW index after compaction error: {evict}");
}
if Self::retry_index_operation_conflict(
&e,
format!("Retryable conflict applying HNSW compaction for {ikb}, retrying"),
)
.await
{
continue;
}
return Err(e);
}
}
Self::ensure_not_cancelled(canceller)?;
if !has_more {
return Ok(());
}
if !txn.closed() {
let _ = txn.cancel().await;
}
}
}
#[cfg(diskann)]
async fn process_diskann_compaction(
&self,
ikb: &IndexKeyBase,
canceller: &CancellationToken,
) -> Result<()> {
loop {
Self::ensure_not_cancelled(canceller)?;
let prepared = {
let txn = Arc::new(self.transaction(Read, Optimistic).await?);
let res: Result<
Option<(
crate::catalog::TableId,
crate::idx::trees::diskann::index::DiskAnnCompactionPlan,
)>,
> = async {
let Some(tb) = txn.get_tb(ikb.ns(), ikb.db(), ikb.table(), None).await? else {
return Ok(None);
};
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix)
if !ix.prepare_remove && matches!(&ix.index, Index::DiskAnn(_)) =>
{
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
let plan =
IndexOperation::prepare_diskann_compaction(&ctx, ikb).await?;
Ok(Some((tb.table_id, plan)))
}
_ => Ok(None),
}
}
.await;
let _ = txn.cancel().await;
res?
};
let Some((_tb, plan)) = prepared else {
return Ok(());
};
if !plan.requires_apply() {
return Ok(());
}
let has_more = plan.has_more();
Self::ensure_not_cancelled(canceller)?;
let txn = Arc::new(self.transaction(Write, Optimistic).await?);
let res: Result<bool> = async {
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix) if !ix.prepare_remove => match &ix.index {
Index::DiskAnn(p) => {
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
IndexOperation::apply_diskann_compaction(
&ctx,
&self.index_stores,
ikb,
&ix,
p,
plan,
)
.await
}
_ => Ok(false),
},
_ => Ok(false),
}
}
.await;
if !txn.closed() {
let _ = txn.cancel().await;
}
match res {
Ok(true) => {}
Ok(false) => return Ok(()),
Err(e) => return Err(e),
}
Self::ensure_not_cancelled(canceller)?;
if !has_more {
return Ok(());
}
}
}
async fn process_fulltext_compaction(
&self,
ikb: &IndexKeyBase,
p: &crate::catalog::FullTextParams,
canceller: &CancellationToken,
) -> Result<()> {
loop {
Self::ensure_not_cancelled(canceller)?;
let plan = {
let txn = self.transaction(Read, Optimistic).await?;
let res = IndexOperation::prepare_fulltext_compaction(
&self.index_stores,
ikb,
&txn,
p,
&self.config.file_allowlist,
)
.await;
let _ = txn.cancel().await;
res?
};
if !plan.has_work() {
return Ok(());
}
let has_more = plan.has_more();
Self::ensure_not_cancelled(canceller)?;
let txn = self.transaction(Write, Optimistic).await?;
let res = async {
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix) if !ix.prepare_remove => match &ix.index {
Index::FullText(p) => {
IndexOperation::apply_fulltext_compaction(
&self.index_stores,
ikb,
&txn,
p,
&self.config.file_allowlist,
plan,
)
.await
}
_ => Ok(false),
},
_ => Ok(false),
}
}
.await;
match res {
Ok(true) => {
if let Err(e) = Self::ensure_not_cancelled(canceller) {
let _ = txn.cancel().await;
return Err(e);
}
#[cfg(test)]
if let Err(e) = maybe_inject_retryable_conflict(
RetryableConflictSite::FullTextCompaction,
self.id,
) {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
format!(
"Retryable conflict committing full-text compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
if let Err(e) = txn.commit().await {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
format!(
"Retryable conflict committing full-text compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
}
Ok(false) => {
let _ = txn.cancel().await;
return Ok(());
}
Err(e) => {
let _ = txn.cancel().await;
if Self::retry_index_operation_conflict(
&e,
format!(
"Retryable conflict applying full-text compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
}
Self::ensure_not_cancelled(canceller)?;
if !has_more {
return Ok(());
}
if !txn.closed() {
let _ = txn.cancel().await;
}
}
}
async fn process_count_compaction(
&self,
ikb: &IndexKeyBase,
canceller: &CancellationToken,
) -> Result<()> {
loop {
Self::ensure_not_cancelled(canceller)?;
let plan = {
let txn = self.transaction(Read, Optimistic).await?;
let res = IndexOperation::prepare_count_compaction(ikb, &txn).await;
let _ = txn.cancel().await;
res?
};
if !plan.has_work() {
return Ok(());
}
let has_more = plan.has_more();
Self::ensure_not_cancelled(canceller)?;
let txn = self.transaction(Write, Optimistic).await?;
let res = async {
match txn
.get_tb_index_by_id(ikb.ns(), ikb.db(), ikb.table(), ikb.index(), None)
.await?
{
Some(ix) if !ix.prepare_remove && matches!(&ix.index, Index::Count(_)) => {
IndexOperation::apply_count_compaction(ikb, &txn, plan).await
}
_ => Ok(false),
}
}
.await;
match res {
Ok(true) => {
if let Err(e) = Self::ensure_not_cancelled(canceller) {
let _ = txn.cancel().await;
return Err(e);
}
#[cfg(test)]
if let Err(e) = maybe_inject_retryable_conflict(
RetryableConflictSite::CountCompaction,
self.id,
) {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
format!(
"Retryable conflict committing count compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
if let Err(e) = txn.commit().await {
if Self::cancel_and_retry_index_operation_conflict(
&txn,
&e,
format!(
"Retryable conflict committing count compaction for {ikb}, retrying"
),
)
.await
{
continue;
}
return Err(e);
}
}
Ok(false) => {
let _ = txn.cancel().await;
return Ok(());
}
Err(e) => {
let _ = txn.cancel().await;
if Self::retry_index_operation_conflict(
&e,
format!("Retryable conflict applying count compaction for {ikb}, retrying"),
)
.await
{
continue;
}
return Err(e);
}
}
Self::ensure_not_cancelled(canceller)?;
if !has_more {
return Ok(());
}
}
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(self))]
pub async fn event_processing(&self, interval: Duration) -> Result<()> {
trace!(target: TARGET, "Attempting event processing process");
let lh = LeaseHandler::new(
self.sequences.clone(),
self.id,
self.transaction_factory.clone(),
TaskLeaseType::EventProcessing,
interval * 2,
)?;
loop {
if !lh.has_lease().await? {
return Ok(());
}
trace!(target: TARGET, "Running event processing process");
if AsyncEventRecord::process_next_events_batch(self, Some(&lh)).await? == 0 {
return Ok(());
}
}
}
pub async fn transaction(&self, write: TransactionType, lock: LockType) -> Result<Transaction> {
self.transaction_factory.transaction(write, lock, self.sequences.clone()).await
}
pub(crate) fn sequences(&self) -> &Sequences {
&self.sequences
}
pub(crate) fn transaction_factory(&self) -> &TransactionFactory {
&self.transaction_factory
}
#[cfg(test)]
#[cfg_attr(not(feature = "kv-mem"), allow(dead_code))]
pub(crate) fn index_builder(&self) -> &IndexBuilder {
&self.index_builder
}
pub fn async_event_trigger(&self) -> &Arc<Notify> {
&self.async_event_trigger
}
pub async fn health_check(&self) -> Result<()> {
let tx = self.transaction(Read, Optimistic).await?;
trace!("Cancelling health check transaction");
match tx.get(&vec![0x00], None).await {
Err(err) => {
let _ = tx.cancel().await;
Err(err)
}
Ok(_) => {
let _ = tx.cancel().await;
Ok(())
}
}
}
pub async fn node_heartbeat_age(&self) -> Result<Duration> {
let tx = self.transaction(Read, Optimistic).await?;
let res = tx.get_node(self.id).await;
let _ = tx.cancel().await;
let node = res?;
let now = self.clock_now();
Ok(Duration::from_millis(now.value.saturating_sub(node.heartbeat.value)))
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute(
&self,
txt: &str,
sess: &Session,
vars: Option<PublicVariables>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
let ast = syn::parse_with_capabilities(txt, &self.capabilities, &self.config)
.map_err(|e| TypesError::validation(e.to_string(), None))?;
self.process(ast, sess, vars).await
}
#[cfg(feature = "gql")]
pub(crate) fn parse_gql(&self, txt: &str) -> std::result::Result<PreparedGqlQuery, TypesError> {
if !self.capabilities.allows_experimental(&ExperimentalTarget::Gql) {
return Err(TypesError::not_allowed(
"Experimental capability `gql` is not enabled".to_string(),
None,
));
}
crate::gql::parse_with_capabilities(txt, &self.capabilities, &self.config)
.map_err(|e| TypesError::validation(e.to_string(), None))
}
#[cfg(feature = "gql")]
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute_gql(
&self,
txt: &str,
sess: &Session,
vars: Option<PublicVariables>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
let plan = self.parse_gql(txt)?;
self.process_gql(plan, sess, vars).await
}
#[cfg(feature = "gql")]
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn process_gql(
&self,
q: PreparedGqlQuery,
sess: &Session,
vars: Option<PublicVariables>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_inner(q.0, sess, vars, None).await
}
#[cfg(feature = "gql")]
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub(crate) async fn process_gql_with_cancel(
&self,
q: PreparedGqlQuery,
sess: &Session,
vars: Option<PublicVariables>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_inner(q.0, sess, vars, Some(cancel)).await
}
#[cfg(feature = "gql")]
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub(crate) async fn process_gql_with_transaction(
&self,
q: PreparedGqlQuery,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_with_transaction_inner(q.0, sess, vars, tx, None).await
}
#[cfg(feature = "gql")]
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub(crate) async fn process_gql_with_transaction_and_cancel(
&self,
q: PreparedGqlQuery,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_with_transaction_inner(q.0, sess, vars, tx, Some(cancel)).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute_with_cancel(
&self,
txt: &str,
sess: &Session,
vars: Option<PublicVariables>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
let ast = syn::parse_with_capabilities(txt, &self.capabilities, &self.config)
.map_err(|e| TypesError::validation(e.to_string(), None))?;
self.process_with_cancel(ast, sess, vars, cancel).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute_with_transaction(
&self,
txt: &str,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
let ast = syn::parse_with_capabilities(txt, &self.capabilities, &self.config)
.map_err(|e| TypesError::validation(e.to_string(), None))?;
self.process_with_transaction(ast, sess, vars, tx).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute_with_transaction_and_cancel(
&self,
txt: &str,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
let ast = syn::parse_with_capabilities(txt, &self.capabilities, &self.config)
.map_err(|e| TypesError::validation(e.to_string(), None))?;
self.process_with_transaction_and_cancel(ast, sess, vars, tx, cancel).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn process_with_transaction(
&self,
ast: Ast,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_with_transaction_inner(ast, sess, vars, tx, None).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn process_with_transaction_and_cancel(
&self,
ast: Ast,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_with_transaction_inner(ast, sess, vars, tx, Some(cancel)).await
}
async fn process_with_transaction_inner(
&self,
ast: Ast,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
cancel: Option<CancelHandle>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_with_transaction_inner(ast.into(), sess, vars, tx, cancel).await
}
async fn process_plan_with_transaction_inner(
&self,
plan: LogicalPlan,
sess: &Session,
vars: Option<PublicVariables>,
tx: Arc<Transaction>,
cancel: Option<CancelHandle>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
if sess.expired() {
return Err(TypesError::not_allowed(
"The session has expired".to_string(),
AuthError::SessionExpired,
));
}
if let Err(e) = self.check_anon(sess) {
return Err(TypesError::not_allowed(
format!("Anonymous access not allowed: {e}"),
AuthError::NotAllowed {
actor: "anonymous".to_owned(),
action: "process".to_owned(),
resource: "query".to_owned(),
},
));
}
let opt = self.setup_options(sess);
let mut ctx = self.setup_ctx().map_err(|e| {
e.downcast::<Error>()
.map(crate::err::into_types_error)
.unwrap_or_else(|e| TypesError::internal(e.to_string()))
})?;
if let Some(cancel) = cancel {
ctx.set_cancellation(&cancel);
}
ctx.attach_session(sess).map_err(crate::err::into_types_error)?;
if let Some(vars) = vars {
ctx.attach_variables(vars.into()).map_err(crate::err::into_types_error)?;
}
if let Some(identity) = ctx.tenant_identity() {
tx.set_tenant_identity(Arc::clone(identity));
}
ctx.set_transaction(tx);
Executor::execute_plan_with_transaction(self, ctx.freeze(), opt, plan).await.map_err(|e| {
e.downcast::<Error>()
.map(crate::err::into_types_error)
.unwrap_or_else(|e| TypesError::internal(e.to_string()))
})
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn execute_import<S>(
&self,
sess: &Session,
vars: Option<PublicVariables>,
query: S,
) -> Result<Vec<QueryResult>>
where
S: Stream<Item = Result<Bytes>>,
{
ensure!(!sess.expired(), Error::ExpiredSession);
self.check_anon(sess).map_err(|_| {
Error::from(IamError::NotAllowed {
actor: "anonymous".to_string(),
action: "process".to_string(),
resource: "query".to_string(),
})
})?;
let opt = self.setup_options(sess);
let mut ctx = self.setup_ctx()?;
ctx.attach_session(sess)?;
if let Some(vars) = vars {
ctx.attach_variables(vars.into())?;
}
let parser_settings = ParserSettings {
files_enabled: ctx.get_capabilities().allows_experimental(&ExperimentalTarget::Files),
surrealism_enabled: ctx
.get_capabilities()
.allows_experimental(&ExperimentalTarget::Surrealism),
..Default::default()
};
let mut statements_stream = StatementStream::new_with_settings(parser_settings);
let mut buffer = BytesMut::new();
let mut parse_size = 4096;
let mut bytes_stream = pin!(query);
let mut complete = false;
let mut filling = true;
let stream = futures::stream::poll_fn(move |cx| {
loop {
while filling {
let bytes = ready!(bytes_stream.as_mut().poll_next(cx));
let bytes = match bytes {
Some(Err(e)) => return Poll::Ready(Some(Err(e))),
Some(Ok(x)) => x,
None => {
complete = true;
filling = false;
break;
}
};
buffer.extend_from_slice(&bytes);
filling = buffer.len() < parse_size
}
if complete {
return match statements_stream.parse_complete(&mut buffer) {
Err(e) => {
Poll::Ready(Some(Err(anyhow::Error::new(Error::InvalidQuery(e)))))
}
Ok(None) => Poll::Ready(None),
Ok(Some(x)) => Poll::Ready(Some(Ok(x))),
};
}
match statements_stream.parse_partial(&mut buffer) {
Err(e) => {
return Poll::Ready(Some(Err(anyhow::Error::new(Error::InvalidQuery(e)))));
}
Ok(Some(x)) => return Poll::Ready(Some(Ok(x))),
Ok(None) => {
if buffer.len() >= parse_size && parse_size < u32::MAX as usize {
parse_size = (parse_size + 1).next_power_of_two();
}
filling = true;
}
}
}
});
Executor::execute_stream(self, Arc::new(ctx), opt, true, stream).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn process(
&self,
ast: Ast,
sess: &Session,
vars: Option<PublicVariables>,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_inner(ast.into(), sess, vars, None).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn process_with_cancel(
&self,
ast: Ast,
sess: &Session,
vars: Option<PublicVariables>,
cancel: CancelHandle,
) -> std::result::Result<Vec<QueryResult>, TypesError> {
self.process_plan_inner(ast.into(), sess, vars, Some(cancel)).await
}
pub(crate) async fn process_plan(
&self,
plan: LogicalPlan,
sess: &Session,
vars: Option<PublicVariables>,
) -> Result<Vec<QueryResult>, TypesError> {
self.process_plan_inner(plan, sess, vars, None).await
}
async fn process_plan_inner(
&self,
plan: LogicalPlan,
sess: &Session,
vars: Option<PublicVariables>,
cancel: Option<CancelHandle>,
) -> Result<Vec<QueryResult>, TypesError> {
if sess.expired() {
return Err(TypesError::not_allowed(
"The session has expired".to_string(),
AuthError::SessionExpired,
));
}
if let Err(e) = self.check_anon(sess) {
return Err(TypesError::not_allowed(
format!("Anonymous access not allowed: {e}"),
AuthError::NotAllowed {
actor: "anonymous".to_owned(),
action: "process".to_owned(),
resource: "query".to_owned(),
},
));
}
let opt = self.setup_options(sess);
let mut ctx = self.setup_ctx().map_err(|e| {
e.downcast::<Error>()
.map(crate::err::into_types_error)
.unwrap_or_else(|e| TypesError::internal(e.to_string()))
})?;
if let Some(cancel) = cancel {
ctx.set_cancellation(&cancel);
}
ctx.attach_session(sess).map_err(crate::err::into_types_error)?;
if let Some(vars) = vars {
ctx.attach_variables(vars.into()).map_err(crate::err::into_types_error)?;
}
Executor::execute_plan(self, ctx.freeze(), opt, plan).await.map_err(|e| {
e.downcast::<Error>()
.map(crate::err::into_types_error)
.unwrap_or_else(|e| TypesError::internal(e.to_string()))
})
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub(crate) async fn evaluate(
&self,
val: &Expr,
sess: &Session,
vars: Option<PublicVariables>,
) -> Result<PublicValue> {
ensure!(!sess.expired(), Error::ExpiredSession);
let mut stack = TreeStack::new();
let opt = self.setup_options(sess);
let mut ctx = self.setup_ctx()?;
if let Some(timeout) = self.dynamic_configuration.get_query_timeout() {
ctx.add_timeout(timeout)?;
}
let txn_type = if val.read_only() {
TransactionType::Read
} else {
TransactionType::Write
};
let txn = self
.transaction(txn_type, Optimistic)
.await?
.with_tenant_identity(Some(Arc::new(crate::observe::TenantIdentity::from_session(
sess,
))))
.enclose();
ctx.set_transaction(Arc::clone(&txn));
ctx.attach_session(sess)?;
if let Some(vars) = vars {
ctx.attach_public_variables(vars)?;
}
let ctx = ctx.freeze();
let res =
stack.enter(|stk| val.compute(stk, &ctx, &opt, None)).finish().await.catch_return();
if res.is_ok() && txn_type == TransactionType::Write {
txn.commit().await?;
} else {
txn.cancel().await?;
};
convert_value_to_public_value(res?)
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn import(&self, sql: &str, sess: &Session) -> Result<Vec<QueryResult>> {
ensure!(!sess.expired(), Error::ExpiredSession);
self.execute(sql, sess, None).await.map_err(|e| anyhow::anyhow!(e))
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn import_stream<S>(&self, sess: &Session, stream: S) -> Result<Vec<QueryResult>>
where
S: Stream<Item = Result<Bytes>>,
{
ensure!(!sess.expired(), Error::ExpiredSession);
self.execute_import(sess, None, stream).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn export(
&self,
sess: &Session,
chn: Sender<Vec<u8>>,
) -> Result<impl Future<Output = Result<()>>> {
let cfg = super::export::Config::default();
self.export_with_config(sess, chn, cfg).await
}
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn export_with_config(
&self,
sess: &Session,
chn: Sender<Vec<u8>>,
cfg: export::Config,
) -> Result<impl Future<Output = Result<()>> + 'static> {
ensure!(!sess.expired(), Error::ExpiredSession);
let (ns, db) = crate::iam::check::check_ns_db(sess)?;
let txn = self.transaction(Read, Optimistic).await?;
let batch_size = self.config.export_batch_size;
Ok(async move {
let res = txn.export(&ns, &db, cfg, batch_size, chn).await;
txn.cancel().await?;
res
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::ds", skip(self, sess))]
#[allow(clippy::needless_pass_by_value)] pub fn check(&self, sess: &Session, action: Action, resource: Resource) -> Result<()> {
ensure!(!sess.expired(), Error::ExpiredSession);
let skip_auth = !self.is_auth_enabled() && sess.au.is_anon();
if !skip_auth {
sess.au.is_allowed(action, &resource)?;
}
Ok(())
}
pub fn setup_options(&self, sess: &Session) -> Options {
Options::new(&self.config)
.with_ns(sess.ns())
.with_db(sess.db())
.with_auth(Arc::clone(&sess.au))
}
pub fn setup_ctx(&self) -> Result<Context> {
let ctx = Context::from_ds(
self.id,
self.auth_enabled,
self.dynamic_configuration.clone(),
self.dynamic_configuration.get_query_timeout(),
self.slow_log.clone(),
Arc::clone(&self.capabilities),
self.index_stores.clone(),
self.index_builder.clone(),
self.sequences.clone(),
Arc::clone(&self.cache),
Arc::clone(&self.function_registry),
#[cfg(feature = "http")]
Arc::clone(&self.http_client),
#[cfg(storage)]
self.temporary_directory.clone(),
self.buckets.clone(),
Arc::clone(&self.config),
#[cfg(feature = "surrealism")]
Arc::clone(&self.surrealism_cache),
)?;
Ok(ctx)
}
pub fn check_anon(&self, sess: &Session) -> Result<(), IamError> {
if self.auth_enabled && sess.au.is_anon() && !self.capabilities.allows_guest_access() {
Err(IamError::NotAllowed {
actor: "anonymous".to_string(),
action: String::new(),
resource: String::new(),
})
} else {
Ok(())
}
}
pub(crate) async fn should_materialize_ns_on_use(
&self,
tx: &Transaction,
auth: &Auth,
ns: &str,
) -> Result<bool> {
if tx.get_ns_by_name(ns, None).await?.is_some() {
return Ok(true);
}
if !self.auth_enabled && auth.is_anon() {
return Ok(true);
}
Ok(auth.is_allowed(Action::Edit, &ResourceKind::Namespace.on_root()).is_ok())
}
pub(crate) async fn should_materialize_db_on_use(
&self,
tx: &Transaction,
auth: &Auth,
ns: &str,
db: &str,
) -> Result<bool> {
if tx.get_db_by_name(ns, db, None).await?.is_some() {
return Ok(true);
}
if !self.auth_enabled && auth.is_anon() {
return Ok(true);
}
if tx.get_ns_by_name(ns, None).await?.is_none()
&& auth.is_allowed(Action::Edit, &ResourceKind::Namespace.on_root()).is_err()
{
return Ok(false);
}
Ok(auth.is_allowed(Action::Edit, &ResourceKind::Database.on_ns(ns)).is_ok())
}
pub async fn process_use(
&self,
ctx: Option<&Context>,
session: &mut Session,
namespace: Option<String>,
database: Option<String>,
) -> std::result::Result<QueryResult, TypesError> {
let new_tx = || async {
self.transaction(Write, Optimistic)
.await
.map_err(|err| TypesError::internal(err.to_string()))
};
let commit_tx = |txn: Transaction| async move {
txn.commit().await.map_err(|err| TypesError::internal(err.to_string()))
};
let query_result = QueryResultBuilder::started_now();
let map_internal = |err: anyhow::Error| TypesError::internal(err.to_string());
match (namespace, database) {
(Some(ns), Some(db)) => {
let tx = new_tx().await?;
let create_ns = self
.should_materialize_ns_on_use(&tx, &session.au, &ns)
.await
.map_err(map_internal)?;
let create_db = create_ns
&& self
.should_materialize_db_on_use(&tx, &session.au, &ns, &db)
.await
.map_err(map_internal)?;
if create_db {
tx.ensure_ns_db(ctx, &ns, &db).await.map_err(map_internal)?;
commit_tx(tx).await?;
} else if create_ns {
tx.get_or_add_ns(ctx, &ns).await.map_err(map_internal)?;
commit_tx(tx).await?;
} else {
let _ = tx.cancel().await;
}
session.ns = Some(ns);
session.db = Some(db);
}
(Some(ns), None) => {
let tx = new_tx().await?;
let create_ns = self
.should_materialize_ns_on_use(&tx, &session.au, &ns)
.await
.map_err(map_internal)?;
if create_ns {
tx.get_or_add_ns(ctx, &ns).await.map_err(map_internal)?;
commit_tx(tx).await?;
} else {
let _ = tx.cancel().await;
}
session.ns = Some(ns);
}
(None, Some(db)) => {
let Some(ns) = session.ns.clone() else {
return Err(TypesError::validation(
"Cannot use database without namespace".to_string(),
None,
));
};
let tx = new_tx().await?;
let create_db = self
.should_materialize_db_on_use(&tx, &session.au, &ns, &db)
.await
.map_err(map_internal)?;
if create_db {
tx.ensure_ns_db(ctx, &ns, &db).await.map_err(map_internal)?;
commit_tx(tx).await?;
} else {
let _ = tx.cancel().await;
}
session.db = Some(db);
}
(None, None) => {
session.ns = None;
session.db = None;
}
}
let value = PublicValue::from_t(object! {
namespace: session.ns.clone(),
database: session.db.clone(),
});
Ok(query_result.finish_with_result(Ok(value)))
}
pub async fn get_db_model(
&self,
ns: &str,
db: &str,
model_name: &str,
model_version: &str,
) -> Result<Option<Arc<crate::catalog::MlModelDefinition>>> {
let tx = self.transaction(Read, Optimistic).await?;
let db = tx.expect_db_by_name(ns, db).await?;
let model = tx
.get_db_model(db.namespace_id, db.database_id, model_name, model_version, None)
.await?;
tx.cancel().await?;
Ok(model)
}
pub async fn invoke_api_handler(
&self,
ns: &str,
db: &str,
path: &str,
session: &Session,
mut req: ApiRequest,
) -> Result<ApiResponse> {
if !session.au.can_access_ns_db(ns, db) {
debug!(
request_id = %req.request_id,
"Custom API request denied: URL namespace/database is outside the authenticated session scope"
);
return Ok(ApiResponse::from_error(ApiError::PermissionDenied, req.request_id.clone()));
}
let tx = Arc::new(self.transaction(TransactionType::Write, LockType::Optimistic).await?);
let db = tx.ensure_ns_db(None, ns, db).await?;
let apis = tx.all_db_apis(db.namespace_id, db.database_id, None).await?;
let segments: Vec<&str> = path.split('/').filter(|x| !x.is_empty()).collect();
let res = match ApiDefinition::find_definition(apis.as_ref(), &segments, req.method) {
Some((api, params)) => {
debug!(
request_id = %req.request_id,
path = %path,
"API definition found, dispatching to process_api_request"
);
req.params = params.try_into()?;
let opt = self.setup_options(session);
let mut ctx = self.setup_ctx()?;
ctx.set_transaction(Arc::clone(&tx));
ctx.attach_session(session)?;
let ctx = &ctx.freeze();
process_api_request(ctx, &opt, api, req).await
}
None => {
trace!(
request_id = %req.request_id,
path = %path,
"No API definition found for path"
);
tx.cancel().await?;
return Ok(ApiResponse::from_error(ApiError::NotFound, req.request_id.clone()));
}
};
if res.is_ok() {
tx.commit().await?;
} else {
tx.cancel().await?;
}
res
}
pub async fn put_ml_model(
&self,
session: &Session,
name: &str,
version: &str,
description: &str,
data: Vec<u8>,
) -> Result<()> {
let ns = session.ns.as_ref().context("Namespace is required")?;
let db = session.db.as_ref().context("Database is required")?;
self.check(session, Action::Edit, ResourceKind::Model.on_db(ns, db))?;
let hash = crate::obs::hash(&data);
let path = get_model_path(ns, db, name, version, &hash);
crate::obs::put(&path, data).await?;
let model = DefineModelStatement {
name: name.to_string().into(),
version: version.to_string().into(),
comment: Expr::Literal(Literal::String(description.into())),
hash: hash.into(),
kind: Default::default(),
permissions: Default::default(),
};
let q = LogicalPlan {
expressions: vec![TopLevelExpr::Expr(Expr::Define(Box::new(DefineStatement::Model(
model,
))))],
};
self.process_plan(q, session, None).await.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
pub fn config(&self) -> Arc<CommonConfig> {
Arc::clone(&self.config)
}
#[cfg(all(feature = "graphql", not(target_family = "wasm")))]
pub async fn graphql_schema(
self: &Arc<Self>,
session: &Session,
) -> Result<async_graphql::dynamic::Schema, crate::graphql::GraphqlError> {
self.graphql_schema_cache.get_schema(self, session).await
}
#[cfg(feature = "http")]
pub fn http_client(&self) -> Arc<HttpClient> {
Arc::clone(&self.http_client)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn endpoint_resolver(&self) -> Arc<dyn crate::dbs::NodeEndpointResolver> {
Arc::new(CatalogNodeEndpointResolver {
transaction_factory: self.transaction_factory.clone(),
sequences: self.sequences.clone(),
})
}
}
#[cfg(not(target_family = "wasm"))]
#[derive(Clone)]
struct CatalogNodeEndpointResolver {
transaction_factory: TransactionFactory,
sequences: Sequences,
}
#[cfg(not(target_family = "wasm"))]
impl std::fmt::Debug for CatalogNodeEndpointResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CatalogNodeEndpointResolver").finish_non_exhaustive()
}
}
#[cfg(not(target_family = "wasm"))]
impl crate::dbs::NodeEndpointResolver for CatalogNodeEndpointResolver {
fn resolve(
&self,
target_node: [u8; 16],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<String>> + Send + '_>> {
Box::pin(async move {
let uuid = Uuid::from_bytes(target_node);
let txn = self
.transaction_factory
.transaction(Read, Optimistic, self.sequences.clone())
.await
.ok()?;
let key = crate::key::root::nd::Nd::new(uuid);
let node: Option<Node> = txn.get(&key, None).await.ok()?;
let _ = txn.cancel().await;
node.and_then(|n| n.http_endpoint)
})
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use std::future::pending;
use super::*;
use crate::catalog::providers::{
CatalogProvider, DatabaseProvider, NamespaceProvider, TableProvider,
};
use crate::iam::verify::verify_root_creds;
use crate::kvs::testing::{
RetryableConflictSite, inject_retryable_conflict, retryable_conflict_count,
};
use crate::types::{PublicValue, PublicVariables};
use crate::val::TableName;
async fn new_index_compaction_test_ds() -> Result<(Datastore, Session)> {
let ds = Datastore::new("memory").await?;
let session = Session::owner().with_ns("test").with_db("test");
let txn = ds.transaction(Write, Pessimistic).await?;
txn.ensure_ns_db(None, "test", "test").await?;
txn.commit().await?;
Ok((ds, session))
}
async fn execute_all(ds: &Datastore, session: &Session, sql: &str) -> Result<()> {
for result in ds.execute(sql, session, None).await? {
result.result?;
}
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn execute_gql_requires_experimental_capability() -> Result<()> {
let ds = Datastore::new("memory").await?;
let ses = Session::owner().with_ns("test").with_db("test");
let err = ds.execute_gql("MATCH (n:person) RETURN n", &ses, None).await.unwrap_err();
assert!(
err.to_string().contains("Experimental capability `gql` is not enabled"),
"unexpected error: {err}"
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn execute_gql_parses_lowers_and_executes() -> Result<()> {
use crate::dbs::capabilities::Targets;
let ds = Datastore::builder()
.with_capabilities(Capabilities::all().with_experimental(Targets::All))
.build_with_path("memory")
.await?;
let ses = Session::owner().with_ns("test").with_db("test");
let txn = ds.transaction(Write, Pessimistic).await?;
txn.ensure_ns_db(None, "test", "test").await?;
txn.commit().await?;
execute_all(&ds, &ses, "CREATE person:tobie SET name = 'Tobie';").await?;
let mut res = ds.execute_gql("MATCH (n:person) RETURN n.name AS name", &ses, None).await?;
assert_eq!(res.len(), 1);
let val = res.remove(0).result?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { name: "Tobie" }]));
let err = ds.execute_gql("MATCH RETURN", &ses, None).await.unwrap_err();
assert!(!err.to_string().contains("experimental"), "unexpected error: {err}");
Ok(())
}
#[cfg(feature = "gql")]
async fn gql_test_ds() -> Result<(Datastore, Session)> {
use crate::dbs::capabilities::Targets;
let ds = Datastore::builder()
.with_capabilities(Capabilities::all().with_experimental(Targets::All))
.build_with_path("memory")
.await?;
let ses = Session::owner().with_ns("test").with_db("test");
let txn = ds.transaction(Write, Pessimistic).await?;
txn.ensure_ns_db(None, "test", "test").await?;
txn.commit().await?;
Ok((ds, ses))
}
#[cfg(feature = "gql")]
async fn run_gql(ds: &Datastore, ses: &Session, query: &str) -> Result<PublicValue> {
let mut res = ds.execute_gql(query, ses, None).await?;
assert_eq!(res.len(), 1, "expected one result for {query:?}");
Ok(res.remove(0).result?)
}
#[cfg(feature = "gql")]
async fn run_gql_err(ds: &Datastore, ses: &Session, query: &str) -> String {
match ds.execute_gql(query, ses, None).await {
Ok(mut res) => match res.remove(0).result {
Ok(value) => panic!("expected {query:?} to fail, got {value:?}"),
Err(e) => e.to_string(),
},
Err(e) => e.to_string(),
}
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_set_updates_and_returns_after_image() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A', age = 30;").await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person) WHERE n.name = 'A' SET n.age = 31 RETURN n.age AS age",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { age: 31 }]));
let val = run_gql(&ds, &ses, "MATCH (n:person) RETURN n.age AS age").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { age: 31 }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_remove_unsets_field_and_returns_empty() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A', age = 30;").await?;
let val = run_gql(&ds, &ses, "MATCH (n:person) REMOVE n.age").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
let val =
run_gql(&ds, &ses, "MATCH (n:person) WHERE n.age = 30 RETURN n.name AS name").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
let val = run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { name: "A" }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_delete_nodetach_errors_with_edges_then_detach_succeeds() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B'; \
RELATE person:1->knows->person:2;",
)
.await?;
let err = run_gql_err(&ds, &ses, "MATCH (n:person) WHERE n.name = 'A' DELETE n").await;
assert!(err.contains("connected edges"), "{err}");
let val =
run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name ORDER BY name").await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { name: "A" },
object! { name: "B" }
])
);
let val = run_gql(&ds, &ses, "MATCH (n:person) WHERE n.name = 'A' DETACH DELETE n").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
let val =
run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name ORDER BY name").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { name: "B" }]));
let val =
run_gql(&ds, &ses, "MATCH (a:person)-[:knows]->(b:person) RETURN a.name AS a").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_insert_node_and_edge() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
let val = run_gql(&ds, &ses, "INSERT (p:person {name: 'A'})").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
run_gql(&ds, &ses, "INSERT (p:person {name: 'B'})").await?;
let val =
run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name ORDER BY name").await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { name: "A" },
object! { name: "B" }
])
);
run_gql(
&ds,
&ses,
"MATCH (a:person WHERE a.name = 'A') MATCH (b:person WHERE b.name = 'B') \
INSERT (a)-[:knows]->(b)",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:person)-[:knows]->(b:person) RETURN a.name AS a, b.name AS b",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { a: "A", b: "B" }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_label_mutation_rejected() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A';").await?;
let err = run_gql_err(&ds, &ses, "MATCH (n:person) SET n:Archived").await;
assert!(err.contains("Label mutation is not supported"), "{err}");
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_set_all_properties_replaces_and_multi_item() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A', age = 30, city = 'L';").await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person) SET n = {name: 'A2', city: 'X'} RETURN n.name AS name, n.city AS city",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![object! { name: "A2", city: "X" }])
);
let val =
run_gql(&ds, &ses, "MATCH (n:person) WHERE n.age = 30 RETURN n.name AS name").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
let val = run_gql(
&ds,
&ses,
"MATCH (n:person) SET n.age = 5, n.city = 'Z' RETURN n.age AS age, n.city AS city",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { age: 5, city: "Z" }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_multi_statement_set_last_wins() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A', age = 1;").await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person) SET n.age = 5 SET n.age = n.age + 1 RETURN n.age AS age",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { age: 6 }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_edge_set_remove_delete() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B'; \
RELATE person:1->knows->person:2 SET since = 2020;",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:person)-[k:knows]->(b:person) SET k.since = 2099 RETURN k.since AS since",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { since: 2099 }]));
let val = run_gql(&ds, &ses, "MATCH (a:person)-[k:knows]->(b:person) DELETE k").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
let val =
run_gql(&ds, &ses, "MATCH (a:person)-[:knows]->(b:person) RETURN a.name AS a").await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_fanout_set_is_consistent_per_row() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"CREATE person:1 SET name = 'A', age = 30; CREATE person:2 SET name = 'B'; \
CREATE person:3 SET name = 'C'; RELATE person:1->knows->person:2; \
RELATE person:1->knows->person:3;",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:person)-[:knows]->(b:person) WHERE a.name = 'A' SET a.age = 7 \
RETURN a.age AS age, b.name AS b ORDER BY b",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { age: 7, b: "B" },
object! { age: 7, b: "C" }
])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_detach_delete_nulls_cascaded_edge_binding() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B'; \
RELATE person:1->knows->person:2 SET since = 2020;",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:person)-[k:knows]->(b:person) WHERE a.name = 'A' DETACH DELETE a \
RETURN a AS a, k AS k, b.name AS b",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![object! {
a: PublicValue::Null,
k: PublicValue::Null,
b: "B"
}])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_insert_left_edge_chain_and_repeated_anon() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B';")
.await?;
run_gql(
&ds,
&ses,
"MATCH (a:person WHERE a.name = 'A') MATCH (b:person WHERE b.name = 'B') \
INSERT (a)<-[:knows]-(b)",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (x:person)-[:knows]->(y:person) RETURN x.name AS x, y.name AS y",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { x: "B", y: "A" }]));
run_gql(
&ds,
&ses,
"INSERT (x:item {n: 1})-[:link]->(y:item {n: 2})-[:link]->(z:item {n: 3})",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:item)-[:link]->(b:item) RETURN a.n AS a, b.n AS b ORDER BY a",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { a: 1, b: 2 },
object! { a: 2, b: 3 }
])
);
run_gql(&ds, &ses, "INSERT (:tag {v: 1}), (:tag {v: 1})").await?;
let val = run_gql(&ds, &ses, "MATCH (t:tag) RETURN t.v AS v ORDER BY v").await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![object! { v: 1 }, object! { v: 1 }])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_mutation_rejection_ledger() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
let cases: &[(&str, &str)] = &[
("MATCH (n:person) SET x.age = 1", "Unknown variable"),
("MATCH p = (a:person)-[:knows]->(b:person) SET p.x = 1", "group or path variable"),
("MATCH (n:person) SET n = {id: 1}", "reserved `id` key"),
("MATCH (a:person)-[k:knows]->(b:person) SET k.out = 1", "reserved `out` key"),
("MATCH (n:person) REMOVE n:Foo", "Label mutation is not supported"),
("MATCH (a:person) INSERT (a:thing)", "already bound"),
("INSERT (x)", "must declare a label"),
("INSERT (a:person)~[:knows]~(b:person)", "Undirected INSERT edges"),
("MATCH (n:person) DELETE n.age", "bound variable"),
];
for (query, expected) in cases {
let err = run_gql_err(&ds, &ses, query).await;
assert!(err.contains(expected), "query {query:?}: expected {expected:?}, got: {err}");
}
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_mutation_respects_record_permissions() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"DEFINE TABLE person SCHEMALESS \
PERMISSIONS FOR select FULL, FOR create NONE, FOR update NONE, FOR delete NONE; \
CREATE person:1 SET name = 'A', age = 30;",
)
.await?;
let rec = Session::for_record(
"test",
"test",
"user",
surrealdb_types::Value::RecordId(surrealdb_types::RecordId::new("user", "tester")),
);
let _ = ds.execute_gql("INSERT (p:person {name: 'B'})", &rec, None).await;
let _ = ds.execute_gql("MATCH (n:person) SET n.age = 99", &rec, None).await;
let _ = ds.execute_gql("MATCH (n:person) DETACH DELETE n", &rec, None).await;
let val =
run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name, n.age AS age").await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![object! { name: "A", age: 30 }])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_nodetach_probe_ignores_select_permissions() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"DEFINE TABLE person SCHEMALESS \
PERMISSIONS FOR select FULL, FOR create FULL, FOR update FULL, FOR delete FULL; \
DEFINE TABLE knows SCHEMALESS \
PERMISSIONS FOR select NONE, FOR create FULL, FOR update FULL, FOR delete FULL; \
CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B'; \
RELATE person:1->knows->person:2;",
)
.await?;
let rec = Session::for_record(
"test",
"test",
"user",
surrealdb_types::Value::RecordId(surrealdb_types::RecordId::new("user", "tester")),
);
let err = run_gql_err(&ds, &rec, "MATCH (n:person WHERE n.name = 'A') DELETE n").await;
assert!(err.contains("connected edges"), "{err}");
let val =
run_gql(&ds, &ses, "MATCH (n:person) RETURN n.name AS name ORDER BY name").await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { name: "A" },
object! { name: "B" }
])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_match_after_set_rereads_live_state() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(
&ds,
&ses,
"CREATE person:1 SET name = 'A', tier = 'bronze'; \
CREATE person:2 SET name = 'B', tier = 'gold';",
)
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person WHERE n.name = 'A') SET n.tier = 'gold' \
MATCH (m:person WHERE m.tier = 'gold') RETURN m.name AS name ORDER BY name",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { name: "A" },
object! { name: "B" }
])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_match_after_delete_rereads_live_state() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B';")
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person WHERE n.name = 'A') DETACH DELETE n \
MATCH (m:person) RETURN m.name AS name ORDER BY name",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { name: "B" }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_match_after_insert_sees_new_node_and_anchors_on_binding() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A', age = 20;").await?;
let val = run_gql(
&ds,
&ses,
"INSERT (a:person {name: 'New', age: 20}) \
MATCH (b:person WHERE b.age = a.age) RETURN b.name AS name ORDER BY name",
)
.await?;
assert_eq!(
val,
PublicValue::Array(surrealdb_types::array![
object! { name: "A" },
object! { name: "New" }
])
);
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_optional_match_after_set_rereads_live_state() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A'; CREATE person:2 SET name = 'B';")
.await?;
let val = run_gql(
&ds,
&ses,
"MATCH (n:person WHERE n.name = 'A') SET n.tier = 'gold' \
OPTIONAL MATCH (m:person WHERE m.tier = 'gold') \
RETURN n.name AS n, m.name AS m",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { n: "A", m: "A" }]));
Ok(())
}
#[cfg(feature = "gql")]
#[tokio::test]
async fn gql_optional_match_after_insert_sees_new_node() -> Result<()> {
let (ds, ses) = gql_test_ds().await?;
execute_all(&ds, &ses, "CREATE person:1 SET name = 'A';").await?;
let val = run_gql(
&ds,
&ses,
"MATCH (a:person WHERE a.name = 'A') INSERT (t:tag {name: 'X'}) \
OPTIONAL MATCH (m:tag) \
RETURN m.name AS m",
)
.await?;
assert_eq!(val, PublicValue::Array(surrealdb_types::array![object! { m: "X" }]));
Ok(())
}
async fn index_key_base(ds: &Datastore, table: &str, index: &str) -> Result<IndexKeyBase> {
let txn = ds.transaction(Read, Optimistic).await?;
let ns = txn.get_ns_by_name("test", None).await?.unwrap();
let db = txn.get_db_by_name("test", "test", None).await?.unwrap();
let table = TableName::from(table);
let ix =
txn.get_tb_index(ns.namespace_id, db.database_id, &table, index, None).await?.unwrap();
txn.cancel().await?;
Ok(IndexKeyBase::new(ns.namespace_id, db.database_id, table, ix.index_id))
}
async fn assert_index_compaction_commit_retry(
site: RetryableConflictSite,
table: &str,
index: &str,
sql: &str,
) -> Result<()> {
let (ds, session) = new_index_compaction_test_ds().await?;
execute_all(&ds, &session, sql).await?;
let ikb = index_key_base(&ds, table, index).await?;
let node_id = ds.id();
let _guard = inject_retryable_conflict(site, node_id);
ds.process_index_compaction(&ikb, CancellationToken::new()).await?;
assert_eq!(retryable_conflict_count(site, node_id), 0);
Ok(())
}
const COUNT_COMPACTION_SQL: &str = "
DEFINE TABLE user SCHEMALESS;
DEFINE INDEX count_idx ON user COUNT;
CREATE user:1 SET name = 'one' RETURN NONE;
CREATE user:2 SET name = 'two' RETURN NONE;
";
const FULLTEXT_COMPACTION_SQL: &str = "
DEFINE ANALYZER simple TOKENIZERS blank FILTERS lowercase;
DEFINE TABLE doc SCHEMALESS;
DEFINE INDEX ft_idx ON doc FIELDS text FULLTEXT ANALYZER simple BM25 HIGHLIGHTS;
CREATE doc:1 SET text = 'alpha beta' RETURN NONE;
CREATE doc:2 SET text = 'beta gamma' RETURN NONE;
";
const HNSW_COMPACTION_SQL: &str = "
DEFINE TABLE vec SCHEMALESS;
DEFINE INDEX hnsw_idx ON vec FIELDS vector HNSW DIMENSION 2 DIST EUCLIDEAN TYPE F32 EFC 16 M 4;
CREATE vec:1 SET vector = [1, 2] RETURN NONE;
CREATE vec:2 SET vector = [2, 3] RETURN NONE;
";
#[tokio::test]
async fn count_index_compaction_retries_commit_conflict() -> Result<()> {
assert_index_compaction_commit_retry(
RetryableConflictSite::CountCompaction,
"user",
"count_idx",
COUNT_COMPACTION_SQL,
)
.await
}
#[tokio::test]
async fn fulltext_index_compaction_retries_commit_conflict() -> Result<()> {
assert_index_compaction_commit_retry(
RetryableConflictSite::FullTextCompaction,
"doc",
"ft_idx",
FULLTEXT_COMPACTION_SQL,
)
.await
}
#[tokio::test]
async fn hnsw_index_compaction_retries_commit_conflict() -> Result<()> {
assert_index_compaction_commit_retry(
RetryableConflictSite::HnswCompaction,
"vec",
"hnsw_idx",
HNSW_COMPACTION_SQL,
)
.await
}
#[tokio::test]
async fn index_compaction_retries_queue_cleanup_commit_conflict() -> Result<()> {
let (ds, session) = new_index_compaction_test_ds().await?;
execute_all(&ds, &session, COUNT_COMPACTION_SQL).await?;
let site = RetryableConflictSite::IndexCompactionQueueCleanup;
let node_id = ds.id();
let _guard = inject_retryable_conflict(site, node_id);
let (_, errors) = Datastore::index_compaction(
Arc::new(ds),
Duration::from_secs(1),
CancellationToken::new(),
)
.await?;
assert_eq!(errors, 0);
assert_eq!(retryable_conflict_count(site, node_id), 0);
Ok(())
}
#[tokio::test]
async fn archive_node_for_shutdown_reports_success() {
let outcome = archive_node_for_shutdown(Duration::from_secs(60), Ok(()));
assert_eq!(outcome, ShutdownNodeDeleteOutcome::Archived);
}
#[tokio::test]
async fn archive_node_for_shutdown_reports_failure() {
let outcome = archive_node_for_shutdown(
Duration::from_secs(60),
Err(anyhow::anyhow!("delete failed")),
);
assert_eq!(outcome, ShutdownNodeDeleteOutcome::Failed);
}
#[tokio::test]
async fn archive_node_for_shutdown_reports_timeout() {
let outcome = archive_node_for_shutdown(
Duration::from_millis(1),
Err(anyhow::Error::new(Error::QueryTimedout(Duration::from_millis(1).into()))),
);
assert_eq!(outcome, ShutdownNodeDeleteOutcome::TimedOut);
}
#[tokio::test]
async fn node_tx_step_cancels_after_timeout() {
let ds = Datastore::new("memory").await.unwrap();
let txn = ds.transaction(Write, Optimistic).await.unwrap();
let timeout_duration = Duration::from_millis(10);
let err = await_node_tx_step(
&txn,
Instant::now() + timeout_duration,
timeout_duration,
None,
pending::<Result<()>>(),
)
.await
.unwrap_err();
assert!(matches!(err.downcast_ref::<Error>(), Some(Error::QueryTimedout(_))));
assert!(txn.closed());
}
#[tokio::test]
async fn node_tx_step_cancels_after_cancellation() {
let ds = Datastore::new("memory").await.unwrap();
let txn = ds.transaction(Write, Optimistic).await.unwrap();
let canceller = CancellationToken::new();
canceller.cancel();
let err = await_node_tx_step(
&txn,
Instant::now() + Duration::from_secs(60),
Duration::from_secs(60),
Some(&canceller),
pending::<Result<()>>(),
)
.await
.unwrap_err();
assert!(matches!(err.downcast_ref::<Error>(), Some(Error::QueryCancelled)));
assert!(txn.closed());
}
#[tokio::test]
async fn node_tx_step_cancels_after_error() {
let ds = Datastore::new("memory").await.unwrap();
let txn = ds.transaction(Write, Optimistic).await.unwrap();
let err = await_node_tx_step(
&txn,
Instant::now() + Duration::from_secs(60),
Duration::from_secs(60),
None,
async { Err::<(), _>(anyhow::anyhow!("step failed")) },
)
.await
.unwrap_err();
assert_eq!(err.to_string(), "step failed");
assert!(txn.closed());
}
#[tokio::test]
async fn node_tx_step_success_leaves_transaction_open() {
let ds = Datastore::new("memory").await.unwrap();
let txn = ds.transaction(Write, Optimistic).await.unwrap();
await_node_tx_step(
&txn,
Instant::now() + Duration::from_secs(60),
Duration::from_secs(60),
None,
async { Ok::<_, anyhow::Error>(()) },
)
.await
.unwrap();
assert!(!txn.closed());
txn.commit().await.unwrap();
assert!(txn.closed());
}
#[tokio::test]
async fn node_heartbeat_age_is_small_after_insert() {
let ds = Datastore::new("memory").await.unwrap();
ds.insert_node().await.unwrap();
let age = ds.node_heartbeat_age().await.unwrap();
assert!(age < Duration::from_secs(5), "heartbeat should be fresh, got {age:?}");
}
#[tokio::test]
async fn node_heartbeat_age_reflects_a_stale_heartbeat() {
let ds = Datastore::new("memory").await.unwrap();
let now = ds.clock_now().value;
let stale = Node::new(
ds.id(),
Timestamp {
value: now.saturating_sub(60_000),
},
false,
);
let key = crate::key::root::nd::new(ds.id());
let txn = ds.transaction(Write, Optimistic).await.unwrap();
txn.set(&key, &stale).await.unwrap();
txn.commit().await.unwrap();
let age = ds.node_heartbeat_age().await.unwrap();
assert!(age >= Duration::from_secs(59), "heartbeat should be stale, got {age:?}");
}
#[tokio::test]
async fn test_setup_superuser() {
let ds = Datastore::new("memory").await.unwrap();
let username = "root";
let password = "root";
{
let txn = ds.transaction(Read, Optimistic).await.unwrap();
assert_eq!(txn.all_root_users(None).await.unwrap().len(), 0);
txn.cancel().await.unwrap();
}
ds.initialise_credentials(username, password).await.unwrap();
{
let txn = ds.transaction(Read, Optimistic).await.unwrap();
assert_eq!(txn.all_root_users(None).await.unwrap().len(), 1);
txn.cancel().await.unwrap();
}
verify_root_creds(&ds, username, password).await.unwrap();
let sql = "DEFINE USER root ON ROOT PASSWORD 'test' ROLES OWNER";
let sess = Session::owner();
ds.execute(sql, &sess, None).await.unwrap();
let pass_hash = {
let txn = ds.transaction(Read, Optimistic).await.unwrap();
let res = txn.expect_root_user(username).await.unwrap().hash.clone();
txn.cancel().await.unwrap();
res
};
ds.initialise_credentials(username, password).await.unwrap();
{
let txn = ds.transaction(Read, Optimistic).await.unwrap();
assert_eq!(pass_hash, txn.expect_root_user(username).await.unwrap().hash.clone());
txn.cancel().await.unwrap();
}
}
#[tokio::test]
pub async fn very_deep_query() -> Result<()> {
use reblessive::{Stack, Stk};
use crate::expr::{BinaryOperator, Expr, Literal};
use crate::kvs::Datastore;
use crate::val::{Number, Value};
let mut stack = Stack::new();
async fn build_query(stk: &mut Stk, depth: usize) -> Expr {
if depth == 0 {
Expr::Binary {
left: Box::new(Expr::Literal(Literal::Integer(1))),
op: BinaryOperator::Add,
right: Box::new(Expr::Literal(Literal::Integer(1))),
}
} else {
let q = stk.run(|stk| build_query(stk, depth - 1)).await;
Expr::Binary {
left: Box::new(q),
op: BinaryOperator::Add,
right: Box::new(Expr::Literal(Literal::Integer(1))),
}
}
}
let val = stack.enter(|stk| build_query(stk, 1000)).finish();
let dbs = Datastore::builder()
.with_capabilities(Capabilities::all())
.build_with_path("memory")
.await
.unwrap();
let opt = Options::new(&dbs.config())
.with_ns(Some("test".into()))
.with_db(Some("test".into()))
.with_max_computation_depth(u32::MAX);
let mut ctx = dbs.setup_ctx()?;
let txn = dbs.transaction(TransactionType::Read, Optimistic).await?.enclose();
ctx.set_transaction(Arc::clone(&txn));
let ctx = ctx.freeze();
let mut stack = reblessive::tree::TreeStack::new();
let res = stack
.enter(|stk| val.compute(stk, &ctx, &opt, None))
.finish()
.await
.catch_return()
.unwrap();
assert_eq!(res, Value::Number(Number::Int(1002)));
txn.cancel().await?;
Ok(())
}
#[tokio::test]
async fn cross_transaction_caching_uuids_updated() -> Result<()> {
let (send, _recv) = crate::channel::bounded(crate::cnf::NOTIFICATIONS_CHANNEL_SIZE);
let ds = Datastore::builder()
.with_capabilities(Capabilities::all())
.with_notify(send)
.build_with_path("memory")
.await?;
let ses = Session::owner().with_ns("test").with_db("test").with_rt(true);
let db = {
let txn = ds.transaction(TransactionType::Write, LockType::Pessimistic).await?;
let db = txn.ensure_ns_db(None, "test", "test").await?;
txn.commit().await?;
db
};
let initial = {
let sql = r"DEFINE TABLE test;".to_owned();
let res = &mut ds.execute(&sql, &ses, None).await?;
assert_eq!(res.len(), 1);
res.remove(0).result.unwrap();
let txn = ds.transaction(TransactionType::Read, LockType::Pessimistic).await?;
let tb = TableName::from("test");
let initial = txn.get_tb(db.namespace_id, db.database_id, &tb, None).await?.unwrap();
txn.cancel().await?;
initial
};
let lqid = {
let sql = r"
DEFINE FIELD test ON test;
DEFINE EVENT test ON test WHEN {} THEN {};
DEFINE TABLE view AS SELECT * FROM test;
DEFINE INDEX test ON test FIELDS test;
LIVE SELECT * FROM test;
"
.to_owned();
let res = &mut ds.execute(&sql, &ses, None).await?;
assert_eq!(res.len(), 5);
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
let lqid = res.remove(0).result?;
assert!(matches!(lqid, PublicValue::Uuid(_)));
lqid
};
let after_define = {
let txn = ds.transaction(TransactionType::Read, LockType::Pessimistic).await?;
let tb = TableName::from("test");
let after_define =
txn.get_tb(db.namespace_id, db.database_id, &tb, None).await?.unwrap();
txn.cancel().await?;
assert_ne!(initial.cache_indexes_ts, after_define.cache_indexes_ts);
assert_ne!(initial.cache_tables_ts, after_define.cache_tables_ts);
assert_ne!(initial.cache_events_ts, after_define.cache_events_ts);
assert_ne!(initial.cache_fields_ts, after_define.cache_fields_ts);
assert_ne!(initial.cache_lives_ts, after_define.cache_lives_ts);
after_define
};
{
let sql = r"
REMOVE FIELD test ON test;
REMOVE EVENT test ON test;
REMOVE TABLE view;
REMOVE INDEX test ON test;
KILL $lqid;
"
.to_owned();
let vars =
PublicVariables::from(BTreeMap::from_iter(map! { "lqid".to_string() => lqid }));
let res = &mut ds.execute(&sql, &ses, Some(vars)).await?;
assert_eq!(res.len(), 5);
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
res.remove(0).result.unwrap();
}
{
let txn = ds.transaction(TransactionType::Read, LockType::Pessimistic).await?;
let tb = TableName::from("test");
let after_remove =
txn.get_tb(db.namespace_id, db.database_id, &tb, None).await?.unwrap();
txn.cancel().await?;
assert_ne!(after_define.cache_fields_ts, after_remove.cache_fields_ts);
assert_ne!(after_define.cache_events_ts, after_remove.cache_events_ts);
assert_ne!(after_define.cache_tables_ts, after_remove.cache_tables_ts);
assert_ne!(after_define.cache_indexes_ts, after_remove.cache_indexes_ts);
assert_ne!(after_define.cache_lives_ts, after_remove.cache_lives_ts);
}
Ok(())
}
}