use std::borrow::Cow;
use surrealdb::Surreal;
use surrealdb::engine::any::Any;
use surrealdb::method::Query;
use surrealdb::opt::auth::{Database, Namespace, Root};
use surrealdb::types::QueryError;
use crate::{Error, Result};
pub mod migrations;
pub type Db = Surreal<Any>;
pub type Transaction = surrealdb::method::Transaction<Any>;
pub trait Executor {
fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any>;
}
impl Executor for Db {
fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any> {
Surreal::query(self, query)
}
}
impl Executor for Transaction {
fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any> {
Transaction::query(self, query)
}
}
pub trait MaybeConflict: From<surrealdb::Error> {
fn is_transaction_conflict(&self) -> bool;
}
impl MaybeConflict for surrealdb::Error {
fn is_transaction_conflict(&self) -> bool {
is_transaction_conflict(self)
}
}
#[doc(hidden)]
pub async fn finish<T, E: MaybeConflict>(
tx: Transaction,
result: std::result::Result<T, E>,
) -> std::result::Result<T, E> {
match result {
Ok(value) => {
tx.commit().await?;
Ok(value)
}
Err(err) => {
let _ = tx.cancel().await;
Err(err)
}
}
}
#[doc(hidden)]
pub async fn backoff(attempt: usize) {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.subsec_nanos() as u64);
let base = 5u64 << attempt.min(6);
let millis = base + nanos % base;
tokio::time::sleep(std::time::Duration::from_millis(millis)).await;
}
#[macro_export]
macro_rules! transaction {
($db:expr, |$tx:ident| $body:block) => {{
let db: &$crate::database::Db = $db;
let mut attempt = 1;
loop {
let result = match db.clone().begin().await {
Ok($tx) => {
let result = async { $body }.await;
$crate::database::finish($tx, result).await
}
Err(err) => Err(err.into()),
};
match result {
Err(err)
if $crate::database::MaybeConflict::is_transaction_conflict(&err)
&& attempt < $crate::database::TRANSACTION_ATTEMPTS =>
{
attempt += 1;
$crate::database::backoff(attempt).await;
}
result => break result,
}
}
}};
}
pub const TRANSACTION_ATTEMPTS: usize = 8;
pub const URL_ENV: &str = "SURREAL_URL";
pub const NAMESPACE_ENV: &str = "SURREAL_NS";
pub const DATABASE_ENV: &str = "SURREAL_DB";
pub const USER_ENV: &str = "SURREAL_USER";
pub const PASSWORD_ENV: &str = "SURREAL_PASS";
pub const AUTH_LEVEL_ENV: &str = "SURREAL_AUTH_LEVEL";
pub const DEFAULT_URL: &str = "ws://127.0.0.1:8000";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthLevel {
Root,
Namespace,
Database,
}
impl std::str::FromStr for AuthLevel {
type Err = String;
fn from_str(value: &str) -> std::result::Result<Self, String> {
match value.trim().to_ascii_lowercase().as_str() {
"root" => Ok(Self::Root),
"namespace" | "ns" => Ok(Self::Namespace),
"database" | "db" => Ok(Self::Database),
_ => Err("expected `root`, `namespace` or `database`".to_owned()),
}
}
}
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
pub url: String,
pub namespace: String,
pub database: String,
pub username: Option<String>,
pub password: Option<String>,
pub auth_level: AuthLevel,
}
impl DatabaseConfig {
pub fn from_env() -> Result<Self> {
let non_empty = |key: &str| std::env::var(key).ok().filter(|v| !v.trim().is_empty());
Ok(Self {
url: crate::env::var_or(URL_ENV, DEFAULT_URL),
namespace: crate::env::var_or(NAMESPACE_ENV, "main"),
database: crate::env::var_or(DATABASE_ENV, "main"),
username: non_empty(USER_ENV),
password: non_empty(PASSWORD_ENV),
auth_level: crate::env::parse_or(AUTH_LEVEL_ENV, AuthLevel::Root)?,
})
}
pub fn envs(&self) -> Vec<(&'static str, String)> {
let level = match self.auth_level {
AuthLevel::Root => "root",
AuthLevel::Namespace => "namespace",
AuthLevel::Database => "database",
};
let mut envs = vec![
(URL_ENV, self.url.clone()),
(NAMESPACE_ENV, self.namespace.clone()),
(DATABASE_ENV, self.database.clone()),
(AUTH_LEVEL_ENV, level.to_owned()),
];
envs.extend(self.username.clone().map(|v| (USER_ENV, v)));
envs.extend(self.password.clone().map(|v| (PASSWORD_ENV, v)));
envs
}
pub fn describe(&self) -> String {
format!("{} {}/{}", self.url, self.namespace, self.database)
}
}
pub async fn connect(config: &DatabaseConfig) -> Result<Db> {
let db = surrealdb::engine::any::connect(config.url.as_str()).await?;
if let Some(username) = &config.username {
let password = config
.password
.clone()
.ok_or(Error::MissingDatabaseSetting(PASSWORD_ENV))?;
let username = username.clone();
match config.auth_level {
AuthLevel::Root => {
db.signin(Root { username, password }).await?;
}
AuthLevel::Namespace => {
db.signin(Namespace {
namespace: config.namespace.clone(),
username,
password,
})
.await?;
}
AuthLevel::Database => {
db.signin(Database {
namespace: config.namespace.clone(),
database: config.database.clone(),
username,
password,
})
.await?;
}
}
}
db.use_ns(&config.namespace)
.use_db(&config.database)
.await?;
Ok(db)
}
pub fn is_transaction_conflict(err: &surrealdb::Error) -> bool {
matches!(err.query_details(), Some(QueryError::TransactionConflict))
|| err.message().contains("can be retried")
}
pub fn is_already_exists(err: &surrealdb::Error) -> bool {
err.is_already_exists() || err.message().contains("already contains")
}