use std::sync::Arc;
use rainier_orm::sea_query::Value;
use rainier_orm::{Dialect, ExecOutcome, Executor, Row, ShardRoute};
use rainier_support::{BoxFuture, Error, Result};
use crate::row::{ColumnRequest, OwnedRow};
use crate::statement::Prepared;
pub trait Connection: Send + Sync + 'static {
fn dialect(&self) -> Dialect;
fn shard_family(&self) -> Option<String> {
None
}
fn allocate_id(&self, shard_key: u64) -> Option<u64> {
let _ = shard_key;
None
}
fn fetch<'a>(
&'a self,
route: ShardRoute,
sql: &'a str,
params: Vec<Value>,
columns: Vec<ColumnRequest>,
) -> BoxFuture<'a, Result<Vec<OwnedRow>>>;
fn execute<'a>(
&'a self,
route: ShardRoute,
sql: &'a str,
params: Vec<Value>,
) -> BoxFuture<'a, Result<Vec<ExecOutcome>>>;
fn fetch_raw<'a>(
&'a self,
route: ShardRoute,
sql: &'a str,
params: Vec<Value>,
) -> BoxFuture<'a, Result<Vec<Box<dyn Row>>>>;
}
#[macro_export]
macro_rules! bind_executor {
($executor:ty) => {
impl $crate::Connection for $executor {
fn dialect(&self) -> $crate::reexport::Dialect {
$crate::reexport::Executor::dialect(self)
}
fn shard_family(&self) -> ::std::option::Option<::std::string::String> {
$crate::reexport::Executor::shard_family(self)
.map(::std::string::ToString::to_string)
}
fn allocate_id(&self, shard_key: u64) -> ::std::option::Option<u64> {
$crate::reexport::Executor::allocate_id(self, shard_key)
}
fn fetch<'a>(
&'a self,
route: $crate::reexport::ShardRoute,
sql: &'a str,
params: ::std::vec::Vec<$crate::reexport::Value>,
columns: ::std::vec::Vec<$crate::reexport::ColumnRequest>,
) -> $crate::reexport::BoxFuture<
'a,
$crate::reexport::Result<::std::vec::Vec<$crate::reexport::OwnedRow>>,
> {
::std::boxed::Box::pin(async move {
let rows =
$crate::reexport::Executor::fetch_all_routed(self, route, sql, params)
.await
.map_err($crate::reexport::Error::from)?;
let mut owned = ::std::vec::Vec::with_capacity(rows.len());
for row in &rows {
owned.push(
$crate::reexport::OwnedRow::snapshot(row.as_ref(), &columns)
.map_err($crate::reexport::Error::from)?,
);
}
::std::result::Result::Ok(owned)
})
}
fn execute<'a>(
&'a self,
route: $crate::reexport::ShardRoute,
sql: &'a str,
params: ::std::vec::Vec<$crate::reexport::Value>,
) -> $crate::reexport::BoxFuture<
'a,
$crate::reexport::Result<::std::vec::Vec<$crate::reexport::ExecOutcome>>,
> {
::std::boxed::Box::pin(async move {
let outcome =
$crate::reexport::Executor::execute_routed(self, route, sql, params)
.await
.map_err($crate::reexport::Error::from)?;
::std::result::Result::Ok(::std::vec![outcome])
})
}
fn fetch_raw<'a>(
&'a self,
route: $crate::reexport::ShardRoute,
sql: &'a str,
params: ::std::vec::Vec<$crate::reexport::Value>,
) -> $crate::reexport::BoxFuture<
'a,
$crate::reexport::Result<
::std::vec::Vec<::std::boxed::Box<dyn $crate::reexport::Row>>,
>,
> {
::std::boxed::Box::pin(async move {
$crate::reexport::Executor::fetch_all_routed(self, route, sql, params)
.await
.map_err($crate::reexport::Error::from)
})
}
}
};
}
#[cfg(feature = "sea-orm-executor")]
crate::bind_executor!(rainier_drivers::sql::SeaOrmExecutor);
#[derive(Clone)]
pub struct Database {
connection: Arc<dyn Connection>,
}
impl Database {
pub fn new(connection: impl Connection) -> Self {
Self { connection: Arc::new(connection) }
}
pub fn from_arc(connection: Arc<dyn Connection>) -> Self {
Self { connection }
}
pub fn connection(&self) -> &Arc<dyn Connection> {
&self.connection
}
pub fn dialect(&self) -> Dialect {
self.connection.dialect()
}
pub fn is_sharded(&self) -> bool {
self.connection.shard_family().is_some()
}
pub fn shard_family(&self) -> Option<String> {
self.connection.shard_family()
}
pub async fn fetch(
&self,
prepared: Prepared,
columns: Vec<ColumnRequest>,
) -> Result<Vec<OwnedRow>> {
self.connection.fetch(prepared.route, &prepared.sql, prepared.params, columns).await
}
pub async fn fetch_all<E: rainier_orm::Entity>(&self, prepared: Prepared) -> Result<Vec<E>> {
let rows = self.fetch(prepared, ColumnRequest::for_entity::<E>()).await?;
rows.iter().map(|row| E::from_row(row).map_err(Error::from)).collect()
}
pub async fn fetch_one<E: rainier_orm::Entity>(&self, prepared: Prepared) -> Result<Option<E>> {
let rows = self.fetch(prepared, ColumnRequest::for_entity::<E>()).await?;
match rows.first() {
Some(row) => Ok(Some(E::from_row(row).map_err(Error::from)?)),
None => Ok(None),
}
}
pub async fn fetch_count(&self, prepared: Prepared) -> Result<u64> {
let columns = vec![ColumnRequest::new("cnt", rainier_orm::ColumnType::BigInt)];
let rows = self.fetch(prepared, columns).await?;
let count = match rows.first() {
Some(row) => row.get_i64("cnt").map_err(Error::from)?.unwrap_or(0),
None => 0,
};
Ok(count.max(0) as u64)
}
pub async fn execute(&self, prepared: Prepared) -> Result<ExecOutcome> {
let outcomes =
self.connection.execute(prepared.route, &prepared.sql, prepared.params).await?;
Ok(outcomes.into_iter().next().unwrap_or_default())
}
pub async fn statement(&self, sql: &str) -> Result<ExecOutcome> {
let outcomes = self.connection.execute(ShardRoute::Global, sql, Vec::new()).await?;
Ok(outcomes.into_iter().next().unwrap_or_default())
}
}
impl std::fmt::Debug for Database {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Database")
.field("dialect", &self.connection.dialect())
.field("shard_family", &self.connection.shard_family())
.finish()
}
}
impl Executor for Database {
fn dialect(&self) -> Dialect {
self.connection.dialect()
}
fn shard_family(&self) -> Option<&str> {
None
}
fn is_sharded(&self) -> bool {
self.connection.shard_family().is_some()
}
fn allocate_id(&self, shard_key: u64) -> Option<u64> {
self.connection.allocate_id(shard_key)
}
async fn fetch_all(
&self,
sql: &str,
params: Vec<Value>,
) -> rainier_orm::Result<Vec<Box<dyn Row>>> {
self.connection.fetch_raw(ShardRoute::Global, sql, params).await.map_err(into_anyhow)
}
async fn execute(&self, sql: &str, params: Vec<Value>) -> rainier_orm::Result<ExecOutcome> {
let outcomes =
self.connection.execute(ShardRoute::Global, sql, params).await.map_err(into_anyhow)?;
Ok(outcomes.into_iter().next().unwrap_or_default())
}
async fn fetch_all_routed(
&self,
route: ShardRoute,
sql: &str,
params: Vec<Value>,
) -> rainier_orm::Result<Vec<Box<dyn Row>>> {
self.connection.fetch_raw(route, sql, params).await.map_err(into_anyhow)
}
async fn execute_routed(
&self,
route: ShardRoute,
sql: &str,
params: Vec<Value>,
) -> rainier_orm::Result<ExecOutcome> {
let outcomes = self.connection.execute(route, sql, params).await.map_err(into_anyhow)?;
Ok(outcomes.into_iter().next().unwrap_or_default())
}
}
fn into_anyhow(error: Error) -> rainier_orm::Error {
anyhow::anyhow!("{error}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::statement;
use crate::testing::{fake_database, MemoryConnection};
#[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
#[orm(table = "posts")]
struct Post {
#[orm(pk, auto_increment)]
id: u64,
title: String,
}
fn row(id: u64, title: &str) -> OwnedRow {
OwnedRow::new().with("id", id).with("title", title)
}
#[test]
fn the_send_query_path_is_actually_send() {
fn assert_send<T: Send>(_: T) {}
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
assert_send(async move {
let prepared = statement::select_all::<Post>(Dialect::Sqlite);
let _: Vec<Post> = db.fetch_all(prepared).await.unwrap();
});
}
#[test]
fn a_database_reports_its_backend() {
let db = Database::new(MemoryConnection::new(Dialect::Postgres));
assert_eq!(db.dialect(), Dialect::Postgres);
assert!(!db.is_sharded());
assert_eq!(db.shard_family(), None);
}
#[tokio::test]
async fn fetch_all_decodes_entities() {
let (db, _) = fake_database(
MemoryConnection::new(Dialect::Sqlite).returning([row(1, "a"), row(2, "b")]),
);
let posts: Vec<Post> =
db.fetch_all(statement::select_all::<Post>(Dialect::Sqlite)).await.unwrap();
assert_eq!(posts.len(), 2);
assert_eq!(posts[1], Post { id: 2, title: "b".into() });
}
#[tokio::test]
async fn fetch_one_returns_none_for_no_rows() {
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let found: Option<Post> = db
.fetch_one(statement::select_by_pk::<Post>(Dialect::Sqlite, 1_i64.into()))
.await
.unwrap();
assert!(found.is_none());
}
#[tokio::test]
async fn fetch_count_reads_the_cnt_column() {
let (db, _) = fake_database(
MemoryConnection::new(Dialect::Sqlite).returning([OwnedRow::new().with("cnt", 42_i64)]),
);
let criteria = crate::Criteria::new();
let prepared = statement::count_matching::<Post>(Dialect::Sqlite, &criteria);
assert_eq!(db.fetch_count(prepared).await.unwrap(), 42);
}
#[tokio::test]
async fn fetch_count_is_zero_when_nothing_comes_back() {
let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let criteria = crate::Criteria::new();
let prepared = statement::count_matching::<Post>(Dialect::Sqlite, &criteria);
assert_eq!(db.fetch_count(prepared).await.unwrap(), 0);
}
#[tokio::test]
async fn statements_and_bindings_reach_the_connection() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
db.execute(statement::delete_by_pk::<Post>(Dialect::Sqlite, 3_i64.into())).await.unwrap();
let sql = connection.last_statement().unwrap();
assert!(sql.starts_with("DELETE FROM"), "{sql}");
assert_eq!(connection.bindings()[0].len(), 1);
}
#[tokio::test]
async fn ddl_runs_through_statement() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
db.statement("CREATE TABLE posts (id INTEGER)").await.unwrap();
assert_eq!(connection.last_statement().as_deref(), Some("CREATE TABLE posts (id INTEGER)"));
}
#[tokio::test]
async fn an_error_survives_the_round_trip() {
let (db, _) =
fake_database(MemoryConnection::new(Dialect::Sqlite).failing("table is locked"));
let err = db.statement("SELECT 1").await.unwrap_err();
assert!(err.message().contains("table is locked"), "{}", err.message());
}
#[tokio::test]
async fn the_orm_surface_is_still_reachable_through_the_executor_impl() {
let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
let outcome = Executor::execute(&db, "DELETE FROM posts", vec![]).await.unwrap();
assert_eq!(outcome.rows_affected, 1);
assert_eq!(connection.last_statement().as_deref(), Some("DELETE FROM posts"));
}
#[test]
fn cloning_shares_one_connection() {
let db = Database::new(MemoryConnection::new(Dialect::MySql));
assert!(Arc::ptr_eq(db.connection(), db.clone().connection()));
}
}