use std::sync::Arc;
#[cfg(feature = "postgres")]
use std::time;
use chrono::Duration;
#[cfg(any(feature = "postgres", feature = "sqlite"))]
use lazy_static::lazy_static;
#[cfg(feature = "postgres")]
use sqlx::postgres::PgPoolOptions;
#[cfg(feature = "sqlite")]
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
pub(crate) mod error;
pub(crate) mod models;
pub(crate) const CANDLE_STABLE_AGE: Duration = Duration::hours(1);
#[cfg(feature = "postgres")]
mod postgres;
mod repositories;
#[cfg(feature = "sqlite")]
mod sqlite;
pub use repositories::{
FundingSettlementsRepositoryRead, OhlcCandlesRepositoryRead, PriceTicksRepositoryRead,
RunningTradesRepositoryRead,
};
use error::{DbError, Result};
#[cfg(feature = "postgres")]
use postgres::{
funding_settlements::PgFundingSettlementsRepo, ohlc_candles::PgOhlcCandlesRepo,
price_ticks::PgPriceTicksRepo, running_trades::PgRunningTradesRepo,
};
use repositories::{
FundingSettlementsRepository, OhlcCandlesRepository, PriceTicksRepository,
RunningTradesRepository,
};
#[cfg(feature = "sqlite")]
use sqlite::{
SqliteFundingSettlementsRepo, SqliteOhlcCandlesRepo, SqlitePriceTicksRepo,
SqliteRunningTradesRepo,
};
#[cfg(feature = "sqlite")]
use std::str::FromStr;
#[cfg(feature = "postgres")]
lazy_static! {
pub static ref DEFAULT_PG_POOL_OPTIONS: PgPoolOptions = PgPoolOptions::new()
.max_connections(10)
.acquire_timeout(time::Duration::from_secs(60));
}
#[cfg(feature = "sqlite")]
lazy_static! {
pub static ref DEFAULT_SQLITE_POOL_OPTIONS: SqlitePoolOptions = SqlitePoolOptions::new();
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum DatabasePoolOptions {
#[cfg(feature = "postgres")]
Postgres(PgPoolOptions),
#[cfg(feature = "sqlite")]
Sqlite(SqlitePoolOptions),
}
#[cfg(feature = "postgres")]
impl From<PgPoolOptions> for DatabasePoolOptions {
fn from(pool_options: PgPoolOptions) -> Self {
Self::Postgres(pool_options)
}
}
#[cfg(feature = "sqlite")]
impl From<SqlitePoolOptions> for DatabasePoolOptions {
fn from(pool_options: SqlitePoolOptions) -> Self {
Self::Sqlite(pool_options)
}
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
impl DatabasePoolOptions {
fn for_database_url(db_url: &str) -> Result<Self> {
let scheme = db_url
.split_once(':')
.map(|(scheme, _)| scheme)
.unwrap_or(db_url);
match scheme {
"postgres" | "postgresql" => {
#[cfg(feature = "postgres")]
{
Ok(Self::Postgres(DEFAULT_PG_POOL_OPTIONS.clone()))
}
#[cfg(not(feature = "postgres"))]
{
Err(DbError::UnsupportedDatabaseUrl(scheme.to_string()))
}
}
"sqlite" => {
#[cfg(feature = "sqlite")]
{
Ok(Self::Sqlite(DEFAULT_SQLITE_POOL_OPTIONS.clone()))
}
#[cfg(not(feature = "sqlite"))]
{
Err(DbError::UnsupportedDatabaseUrl(scheme.to_string()))
}
}
_ => Err(DbError::UnsupportedDatabaseUrl(scheme.to_string())),
}
}
}
pub struct Database {
pub(crate) ohlc_candles: Box<dyn OhlcCandlesRepository>,
pub(crate) price_ticks: Box<dyn PriceTicksRepository>,
pub(crate) running_trades: Box<dyn RunningTradesRepository>,
pub(crate) funding_settlements: Box<dyn FundingSettlementsRepository>,
}
impl Database {
fn from_repositories(
ohlc_candles: Box<dyn OhlcCandlesRepository>,
price_ticks: Box<dyn PriceTicksRepository>,
running_trades: Box<dyn RunningTradesRepository>,
funding_settlements: Box<dyn FundingSettlementsRepository>,
) -> Arc<Self> {
Arc::new(Self {
ohlc_candles,
price_ticks,
running_trades,
funding_settlements,
})
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub async fn new(db_url: &str) -> Result<Arc<Self>> {
let pool_options = DatabasePoolOptions::for_database_url(db_url)?;
Self::with_pool_options(db_url, pool_options).await
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub async fn with_pool_options(
db_url: &str,
pool_options: impl Into<DatabasePoolOptions>,
) -> Result<Arc<Self>> {
match pool_options.into() {
#[cfg(feature = "postgres")]
DatabasePoolOptions::Postgres(pool_options) => {
Self::postgres_with_pool_options(db_url, pool_options).await
}
#[cfg(feature = "sqlite")]
DatabasePoolOptions::Sqlite(pool_options) => {
Self::sqlite_with_pool_options(db_url, pool_options).await
}
}
}
#[cfg(feature = "postgres")]
async fn postgres_with_pool_options(
db_url: &str,
pool_options: PgPoolOptions,
) -> Result<Arc<Self>> {
let pool = pool_options
.connect(db_url)
.await
.map_err(DbError::Connection)?;
sqlx::migrate!("./migrations/postgres")
.run(&pool)
.await
.map_err(DbError::Migration)?;
let pool = Arc::new(pool);
let ohlc_candles = Box::new(PgOhlcCandlesRepo::new(pool.clone()));
let price_ticks = Box::new(PgPriceTicksRepo::new(pool.clone()));
let running_trades = Box::new(PgRunningTradesRepo::new(pool.clone()));
let funding_settlements = Box::new(PgFundingSettlementsRepo::new(pool.clone()));
Ok(Self::from_repositories(
ohlc_candles,
price_ticks,
running_trades,
funding_settlements,
))
}
#[cfg(feature = "sqlite")]
async fn sqlite_with_pool_options(
db_url: &str,
pool_options: SqlitePoolOptions,
) -> Result<Arc<Self>> {
let connect_options = SqliteConnectOptions::from_str(db_url)
.map_err(DbError::Connection)?
.create_if_missing(true);
let pool = pool_options
.connect_with(connect_options)
.await
.map_err(DbError::Connection)?;
sqlx::migrate!("./migrations/sqlite")
.run(&pool)
.await
.map_err(DbError::Migration)?;
let pool = Arc::new(pool);
let ohlc_candles = Box::new(SqliteOhlcCandlesRepo::new(pool.clone()));
let price_ticks = Box::new(SqlitePriceTicksRepo::new(pool.clone()));
let running_trades = Box::new(SqliteRunningTradesRepo::new(pool.clone()));
let funding_settlements = Box::new(SqliteFundingSettlementsRepo::new(pool.clone()));
Ok(Self::from_repositories(
ohlc_candles,
price_ticks,
running_trades,
funding_settlements,
))
}
pub fn ohlc_candles(&self) -> &(dyn OhlcCandlesRepositoryRead + '_) {
self.ohlc_candles.as_ref()
}
pub fn price_ticks(&self) -> &(dyn PriceTicksRepositoryRead + '_) {
self.price_ticks.as_ref()
}
pub fn funding_settlements(&self) -> &(dyn FundingSettlementsRepositoryRead + '_) {
self.funding_settlements.as_ref()
}
pub fn running_trades(&self) -> &(dyn RunningTradesRepositoryRead + '_) {
self.running_trades.as_ref()
}
}
#[cfg(all(test, feature = "sqlite"))]
mod sqlite_tests {
use std::{env, fs};
use uuid::Uuid;
use super::*;
#[tokio::test]
async fn database_new_sqlite_creates_file_and_runs_migrations() {
let path = env::temp_dir().join(format!("quantoxide-{}.sqlite", Uuid::new_v4()));
let db_url = format!("sqlite:{}", path.display());
assert!(!path.exists());
let db = Database::new(&db_url).await.unwrap();
assert!(path.exists());
assert_eq!(db.price_ticks().get_latest_entry().await.unwrap(), None);
drop(db);
for suffix in ["", "-shm", "-wal"] {
let _ = fs::remove_file(format!("{}{}", path.display(), suffix));
}
}
}