use parking_lot::RwLock;
use std::sync::OnceLock;
use std::time::Duration;
use crate::database::Database;
use crate::error::Result;
use crate::migration::Migration;
use crate::{tide_info, tide_warn};
static GLOBAL_CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
static SCHEMA_FILE_PATH: OnceLock<String> = OnceLock::new();
pub type FileUrlGenerator = fn(field_name: &str, file: &crate::attachments::FileAttachment) -> String;
static GLOBAL_FILE_URL_GENERATOR: OnceLock<FileUrlGenerator> = OnceLock::new();
static GLOBAL_POOL_CONFIG: OnceLock<PoolConfig> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DatabaseType {
#[default]
Postgres,
MySQL,
MariaDB,
SQLite,
}
impl DatabaseType {
pub fn is_mysql_compatible(&self) -> bool {
matches!(self, DatabaseType::MySQL | DatabaseType::MariaDB)
}
pub fn default_port(&self) -> u16 {
match self {
DatabaseType::Postgres => 5432,
DatabaseType::MySQL | DatabaseType::MariaDB => 3306,
DatabaseType::SQLite => 0, }
}
pub fn url_scheme(&self) -> &'static str {
match self {
DatabaseType::Postgres => "postgres",
DatabaseType::MySQL => "mysql",
DatabaseType::MariaDB => "mariadb",
DatabaseType::SQLite => "sqlite",
}
}
pub fn supports_json(&self) -> bool {
match self {
DatabaseType::Postgres => true,
DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_native_json_operators(&self) -> bool {
match self {
DatabaseType::Postgres => true, DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_arrays(&self) -> bool {
matches!(self, DatabaseType::Postgres)
}
pub fn supports_returning(&self) -> bool {
match self {
DatabaseType::Postgres => true,
DatabaseType::MySQL => false,
DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_upsert(&self) -> bool {
match self {
DatabaseType::Postgres => true, DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_fulltext_search(&self) -> bool {
match self {
DatabaseType::Postgres => true, DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_window_functions(&self) -> bool {
match self {
DatabaseType::Postgres => true,
DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_cte(&self) -> bool {
match self {
DatabaseType::Postgres => true,
DatabaseType::MySQL | DatabaseType::MariaDB => true, DatabaseType::SQLite => true, }
}
pub fn supports_schemas(&self) -> bool {
match self {
DatabaseType::Postgres => true,
DatabaseType::MySQL | DatabaseType::MariaDB => false, DatabaseType::SQLite => false,
}
}
pub fn optimal_batch_size(&self) -> usize {
match self {
DatabaseType::Postgres => 1000,
DatabaseType::MySQL | DatabaseType::MariaDB => 500, DatabaseType::SQLite => 100, }
}
pub fn param_style(&self) -> &'static str {
match self {
DatabaseType::Postgres => "$", DatabaseType::MySQL | DatabaseType::MariaDB => "?",
DatabaseType::SQLite => "?",
}
}
pub fn quote_char(&self) -> char {
match self {
DatabaseType::Postgres | DatabaseType::SQLite => '"',
DatabaseType::MySQL | DatabaseType::MariaDB => '`',
}
}
pub fn from_url(url: &str) -> Option<Self> {
let url_lower = url.to_lowercase();
if url_lower.starts_with("postgres://") || url_lower.starts_with("postgresql://") {
Some(DatabaseType::Postgres)
} else if url_lower.starts_with("mariadb://") {
Some(DatabaseType::MariaDB)
} else if url_lower.starts_with("mysql://") {
Some(DatabaseType::MySQL)
} else if url_lower.starts_with("sqlite:") {
Some(DatabaseType::SQLite)
} else {
None
}
}
}
impl std::fmt::Display for DatabaseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DatabaseType::Postgres => write!(f, "PostgreSQL"),
DatabaseType::MySQL => write!(f, "MySQL"),
DatabaseType::MariaDB => write!(f, "MariaDB"),
DatabaseType::SQLite => write!(f, "SQLite"),
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub languages: Vec<String>,
pub fallback_language: String,
pub hidden_attributes: Vec<String>,
pub soft_delete_by_default: bool,
pub file_base_url: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
languages: vec!["en".to_string()],
fallback_language: "en".to_string(),
hidden_attributes: vec![],
soft_delete_by_default: false,
file_base_url: None,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn global() -> Config {
GLOBAL_CONFIG
.get_or_init(|| RwLock::new(Config::default()))
.read()
.clone()
}
#[inline]
fn with_global<T>(f: impl FnOnce(&Config) -> T) -> T {
let lock = GLOBAL_CONFIG.get_or_init(|| RwLock::new(Config::default()));
let guard = lock.read();
f(&guard)
}
pub fn get_languages() -> Vec<String> {
Self::with_global(|c| c.languages.clone())
}
pub fn get_fallback_language() -> String {
Self::with_global(|c| c.fallback_language.clone())
}
pub fn get_hidden_attributes() -> Vec<String> {
Self::with_global(|c| c.hidden_attributes.clone())
}
pub fn is_soft_delete_default() -> bool {
Self::with_global(|c| c.soft_delete_by_default)
}
pub fn get_file_base_url() -> Option<String> {
Self::with_global(|c| c.file_base_url.clone())
}
pub fn get_file_url_generator() -> FileUrlGenerator {
GLOBAL_FILE_URL_GENERATOR
.get()
.copied()
.unwrap_or(Self::default_file_url_generator)
}
pub fn set_file_url_generator(generator: FileUrlGenerator) {
if GLOBAL_FILE_URL_GENERATOR.set(generator).is_err() {
tide_warn!("File URL generator already set, ignoring subsequent call");
}
}
#[inline]
pub fn default_file_url_generator(_field_name: &str, file: &crate::attachments::FileAttachment) -> String {
if let Some(base_url) = Self::get_file_base_url() {
let base = base_url.trim_end_matches('/');
let key = file.key.trim_start_matches('/');
format!("{}/{}", base, key)
} else {
file.key.clone()
}
}
#[inline]
pub fn generate_file_url(field_name: &str, file: &crate::attachments::FileAttachment) -> String {
Self::get_file_url_generator()(field_name, file)
}
}
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_connections: u32,
pub min_connections: u32,
pub connect_timeout: Duration,
pub idle_timeout: Duration,
pub max_lifetime: Duration,
pub acquire_timeout: Duration,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_connections: 10,
min_connections: 1,
connect_timeout: Duration::from_secs(8),
idle_timeout: Duration::from_secs(600), max_lifetime: Duration::from_secs(1800), acquire_timeout: Duration::from_secs(8),
}
}
}
pub struct TideConfig {
config: Config,
database_type: Option<DatabaseType>,
database_url: Option<String>,
pool: PoolConfig,
sync_enabled: bool,
force_sync: bool,
schema_file: Option<String>,
migrations: Vec<Box<dyn Migration>>,
run_migrations: bool,
seeds: Vec<Box<dyn crate::seeding::Seed>>,
run_seeds: bool,
encryption_key: Option<String>,
token_encoder: Option<crate::tokenization::TokenEncoder>,
token_decoder: Option<crate::tokenization::TokenDecoder>,
}
static GLOBAL_DB_TYPE: OnceLock<DatabaseType> = OnceLock::new();
impl TideConfig {
pub fn init() -> Self {
Self {
config: Config::default(),
database_type: None,
database_url: None,
pool: PoolConfig::default(),
sync_enabled: false,
force_sync: false,
schema_file: None,
migrations: Vec::new(),
run_migrations: false,
seeds: Vec::new(),
run_seeds: false,
encryption_key: None,
token_encoder: None,
token_decoder: None,
}
}
pub fn migration<M: Migration + 'static>(mut self, migration: M) -> Self {
self.migrations.push(Box::new(migration));
self
}
pub fn migrations<T: RegisterMigrations>(mut self) -> Self {
self.migrations.extend(T::collect());
self
}
pub fn run_migrations(mut self, enabled: bool) -> Self {
self.run_migrations = enabled;
self
}
pub fn seed<S: crate::seeding::Seed + 'static>(mut self, seed: S) -> Self {
self.seeds.push(Box::new(seed));
self
}
pub fn seeds<T: RegisterSeeds>(mut self) -> Self {
self.seeds.extend(T::collect());
self
}
pub fn run_seeds(mut self, enabled: bool) -> Self {
self.run_seeds = enabled;
self
}
pub fn sync(mut self, enabled: bool) -> Self {
self.sync_enabled = enabled;
self
}
pub fn models<T: crate::sync::RegisterModels>(self) -> Self {
T::register_all();
self
}
pub fn force_sync(mut self, enabled: bool) -> Self {
self.force_sync = enabled;
self
}
pub fn schema_file(mut self, path: &str) -> Self {
self.schema_file = Some(path.to_string());
self
}
pub fn database_type(mut self, db_type: DatabaseType) -> Self {
self.database_type = Some(db_type);
self
}
pub fn database(mut self, url: &str) -> Self {
self.database_url = Some(url.to_string());
self
}
pub fn max_connections(mut self, n: u32) -> Self {
self.pool.max_connections = n;
self
}
pub fn min_connections(mut self, n: u32) -> Self {
self.pool.min_connections = n;
self
}
pub fn connect_timeout(mut self, duration: Duration) -> Self {
self.pool.connect_timeout = duration;
self
}
pub fn idle_timeout(mut self, duration: Duration) -> Self {
self.pool.idle_timeout = duration;
self
}
pub fn max_lifetime(mut self, duration: Duration) -> Self {
self.pool.max_lifetime = duration;
self
}
pub fn acquire_timeout(mut self, duration: Duration) -> Self {
self.pool.acquire_timeout = duration;
self
}
pub fn languages(mut self, langs: &[&str]) -> Self {
self.config.languages = langs.iter().map(|s| s.to_string()).collect();
self
}
pub fn fallback_language(mut self, lang: &str) -> Self {
self.config.fallback_language = lang.to_string();
self
}
pub fn hidden_attributes(mut self, attrs: &[&str]) -> Self {
self.config.hidden_attributes = attrs.iter().map(|s| s.to_string()).collect();
self
}
pub fn soft_delete_by_default(mut self, enabled: bool) -> Self {
self.config.soft_delete_by_default = enabled;
self
}
pub fn file_base_url(mut self, url: &str) -> Self {
self.config.file_base_url = Some(url.to_string());
self
}
pub fn file_url_generator(self, generator: FileUrlGenerator) -> Self {
Config::set_file_url_generator(generator);
self
}
pub fn encryption_key(mut self, key: &str) -> Self {
self.encryption_key = Some(key.to_string());
self
}
pub fn token_encoder(mut self, encoder: crate::tokenization::TokenEncoder) -> Self {
self.token_encoder = Some(encoder);
self
}
pub fn token_decoder(mut self, decoder: crate::tokenization::TokenDecoder) -> Self {
self.token_decoder = Some(decoder);
self
}
pub async fn connect(self) -> Result<&'static Database> {
let config = GLOBAL_CONFIG.get_or_init(|| RwLock::new(Config::default()));
*config.write() = self.config;
if let Some(key) = &self.encryption_key {
crate::tokenization::TokenConfig::set_encryption_key(key);
}
if let Some(encoder) = self.token_encoder {
crate::tokenization::TokenConfig::set_encoder(encoder);
}
if let Some(decoder) = self.token_decoder {
crate::tokenization::TokenConfig::set_decoder(decoder);
}
let url = self.database_url.ok_or_else(|| {
crate::error::Error::configuration(
"Database URL is required. Use .database(\"postgres://...\") to set it."
)
})?;
let mut db_type = match self.database_type {
Some(t) => t,
None => DatabaseType::from_url(&url).ok_or_else(|| {
crate::error::Error::configuration(
"Could not detect database type from URL. \
Use .database_type(DatabaseType::Postgres) to set it explicitly."
)
})?,
};
let connect_url = if url.to_lowercase().starts_with("mariadb://") {
format!("mysql://{}", &url[url.find("://").unwrap() + 3..])
} else {
url.clone()
};
let _ = GLOBAL_POOL_CONFIG.set(self.pool.clone());
let db = Database::builder()
.url(connect_url)
.max_connections(self.pool.max_connections)
.min_connections(self.pool.min_connections)
.connect_timeout(self.pool.connect_timeout)
.idle_timeout(self.pool.idle_timeout)
.max_lifetime(self.pool.max_lifetime)
.build()
.await?;
if db_type == DatabaseType::MySQL {
if let Ok(version) = Self::detect_server_version(&db).await {
if version.to_lowercase().contains("mariadb") {
db_type = DatabaseType::MariaDB;
tide_info!("Auto-detected MariaDB server: {}", version);
}
}
}
let _ = GLOBAL_DB_TYPE.set(db_type);
let db_ref = Database::set_global(db)?;
if self.run_migrations && !self.migrations.is_empty() {
let mut migrator = crate::migration::Migrator::new();
for migration in self.migrations {
migrator = migrator.add_boxed(migration);
}
let result = migrator.run().await?;
if result.has_applied() {
tide_info!("{}", result);
}
}
if self.run_seeds && !self.seeds.is_empty() {
let mut seeder = crate::seeding::Seeder::new();
for seed in self.seeds {
seeder = seeder.add_boxed(seed);
}
let result = seeder.run().await?;
if result.has_executed() {
tide_info!("{}", result);
}
}
if self.sync_enabled {
crate::sync::sync_database_with_options(db_ref, self.force_sync).await?;
}
if let Some(path) = &self.schema_file {
let _ = SCHEMA_FILE_PATH.set(path.clone());
crate::schema::SchemaWriter::write_schema(path).await?;
}
Ok(db_ref)
}
pub fn apply(self) {
let config = GLOBAL_CONFIG.get_or_init(|| RwLock::new(Config::default()));
*config.write() = self.config;
if let Some(db_type) = self.database_type {
let _ = GLOBAL_DB_TYPE.set(db_type);
}
}
pub fn db() -> crate::error::Result<&'static Database> {
crate::database::require_db()
}
pub fn try_db() -> Option<&'static Database> {
crate::database::try_db()
}
pub fn is_connected() -> bool {
crate::database::has_global_db()
}
pub fn get_database_type() -> Option<DatabaseType> {
GLOBAL_DB_TYPE.get().copied()
}
pub fn is_postgres() -> bool {
GLOBAL_DB_TYPE.get() == Some(&DatabaseType::Postgres)
}
pub fn is_mysql() -> bool {
GLOBAL_DB_TYPE.get() == Some(&DatabaseType::MySQL)
}
pub fn is_mariadb() -> bool {
GLOBAL_DB_TYPE.get() == Some(&DatabaseType::MariaDB)
}
pub fn is_mysql_compatible() -> bool {
matches!(GLOBAL_DB_TYPE.get(), Some(DatabaseType::MySQL) | Some(DatabaseType::MariaDB))
}
pub fn is_sqlite() -> bool {
GLOBAL_DB_TYPE.get() == Some(&DatabaseType::SQLite)
}
pub fn current() -> Config {
Config::global()
}
pub fn pool_config() -> PoolConfig {
GLOBAL_POOL_CONFIG.get().cloned().unwrap_or_default()
}
pub fn schema_file_path() -> Option<&'static str> {
SCHEMA_FILE_PATH.get().map(|s| s.as_str())
}
pub fn write_schema_with_generator(generator: &crate::schema::SchemaGenerator) -> std::io::Result<()> {
let Some(path) = SCHEMA_FILE_PATH.get() else {
return Ok(()); };
let sql = generator.generate();
std::fs::write(path, sql)?;
Ok(())
}
pub fn write_schema_sql(sql: &str) -> std::io::Result<()> {
let Some(path) = SCHEMA_FILE_PATH.get() else {
return Ok(()); };
std::fs::write(path, sql)?;
Ok(())
}
async fn detect_server_version(db: &Database) -> Result<String> {
use crate::internal::{ConnectionTrait, Statement, DbBackend};
let conn = db.__internal_connection();
let backend = conn.get_database_backend();
if backend != DbBackend::MySql {
return Err(crate::error::Error::internal("Not a MySQL-type connection"));
}
let stmt = Statement::from_string(backend, "SELECT VERSION() AS version".to_string());
let result = conn.query_one_raw(stmt)
.await
.map_err(|e| crate::error::Error::query(e.to_string()))?;
match result {
Some(row) => {
let version: String = row.try_get("", "version")
.map_err(|e| crate::error::Error::query(e.to_string()))?;
Ok(version)
}
None => Err(crate::error::Error::query("Could not retrieve server version")),
}
}
}
pub trait RegisterMigrations {
fn collect() -> Vec<Box<dyn Migration>>;
}
impl RegisterMigrations for () {
fn collect() -> Vec<Box<dyn Migration>> {
Vec::new()
}
}
macro_rules! impl_register_tuples {
($first:ident) => {
impl<$first: Migration + Default + 'static> RegisterMigrations for ($first,) {
fn collect() -> Vec<Box<dyn Migration>> {
vec![Box::new($first::default())]
}
}
impl<$first: Seed + Default + 'static> RegisterSeeds for ($first,) {
fn collect() -> Vec<Box<dyn Seed>> {
vec![Box::new($first::default())]
}
}
};
($first:ident, $($rest:ident),+) => {
impl_register_tuples!($($rest),+);
impl<$first: Migration + Default + 'static, $($rest: Migration + Default + 'static),+>
RegisterMigrations for ($first, $($rest),+)
{
fn collect() -> Vec<Box<dyn Migration>> {
vec![Box::new($first::default()), $(Box::new($rest::default())),+]
}
}
impl<$first: Seed + Default + 'static, $($rest: Seed + Default + 'static),+>
RegisterSeeds for ($first, $($rest),+)
{
fn collect() -> Vec<Box<dyn Seed>> {
vec![Box::new($first::default()), $(Box::new($rest::default())),+]
}
}
};
}
impl_register_tuples!(
T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18,
T19, T20, T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31, T32, T33, T34, T35,
T36, T37, T38, T39, T40, T41, T42, T43, T44, T45, T46, T47, T48, T49, T50, T51, T52,
T53, T54, T55, T56, T57, T58, T59, T60, T61, T62, T63, T64, T65, T66, T67, T68, T69,
T70, T71, T72, T73, T74, T75, T76, T77, T78, T79, T80, T81, T82, T83, T84, T85, T86,
T87, T88, T89, T90, T91, T92, T93, T94, T95, T96, T97, T98, T99, T100, T101, T102,
T103, T104, T105, T106, T107, T108, T109, T110, T111, T112, T113, T114, T115, T116,
T117, T118, T119, T120, T121, T122, T123, T124, T125, T126, T127, T128, T129, T130,
T131, T132, T133, T134, T135, T136, T137, T138, T139, T140, T141, T142, T143, T144,
T145, T146, T147, T148, T149, T150, T151, T152, T153, T154, T155, T156, T157, T158,
T159, T160, T161, T162, T163, T164, T165, T166, T167, T168, T169, T170, T171, T172,
T173, T174, T175, T176, T177, T178, T179, T180, T181, T182, T183, T184, T185, T186,
T187, T188, T189, T190, T191, T192, T193, T194, T195, T196, T197, T198, T199, T200
);
use crate::seeding::Seed;
pub trait RegisterSeeds {
fn collect() -> Vec<Box<dyn Seed>>;
}
impl RegisterSeeds for () {
fn collect() -> Vec<Box<dyn Seed>> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.languages, vec!["en".to_string()]);
assert_eq!(config.fallback_language, "en");
}
#[test]
fn test_config_builder() {
TideConfig::init()
.languages(&["en", "fr"])
.fallback_language("fr")
.apply();
let config = Config::global();
assert!(config.languages.contains(&"fr".to_string()));
assert_eq!(config.fallback_language, "fr");
}
#[test]
fn test_database_type_from_url() {
assert_eq!(
DatabaseType::from_url("postgres://localhost/test"),
Some(DatabaseType::Postgres)
);
assert_eq!(
DatabaseType::from_url("postgresql://localhost/test"),
Some(DatabaseType::Postgres)
);
assert_eq!(
DatabaseType::from_url("mysql://localhost/test"),
Some(DatabaseType::MySQL)
);
assert_eq!(
DatabaseType::from_url("mariadb://localhost/test"),
Some(DatabaseType::MariaDB)
);
assert_eq!(
DatabaseType::from_url("sqlite:./test.db"),
Some(DatabaseType::SQLite)
);
assert_eq!(
DatabaseType::from_url("sqlite::memory:"),
Some(DatabaseType::SQLite)
);
assert_eq!(
DatabaseType::from_url("invalid://localhost"),
None
);
}
#[test]
fn test_database_type_supports_json() {
assert!(DatabaseType::Postgres.supports_json());
assert!(DatabaseType::MySQL.supports_json());
assert!(DatabaseType::MariaDB.supports_json());
assert!(DatabaseType::SQLite.supports_json());
}
#[test]
fn test_database_type_supports_arrays() {
assert!(DatabaseType::Postgres.supports_arrays());
assert!(!DatabaseType::MySQL.supports_arrays());
assert!(!DatabaseType::MariaDB.supports_arrays());
assert!(!DatabaseType::SQLite.supports_arrays());
}
#[test]
fn test_database_type_supports_returning() {
assert!(DatabaseType::Postgres.supports_returning());
assert!(!DatabaseType::MySQL.supports_returning());
assert!(DatabaseType::MariaDB.supports_returning()); assert!(DatabaseType::SQLite.supports_returning());
}
#[test]
fn test_database_type_supports_upsert() {
assert!(DatabaseType::Postgres.supports_upsert());
assert!(DatabaseType::MySQL.supports_upsert());
assert!(DatabaseType::MariaDB.supports_upsert());
assert!(DatabaseType::SQLite.supports_upsert());
}
#[test]
fn test_database_type_supports_fulltext_search() {
assert!(DatabaseType::Postgres.supports_fulltext_search());
assert!(DatabaseType::MySQL.supports_fulltext_search());
assert!(DatabaseType::MariaDB.supports_fulltext_search());
assert!(DatabaseType::SQLite.supports_fulltext_search());
}
#[test]
fn test_database_type_supports_window_functions() {
assert!(DatabaseType::Postgres.supports_window_functions());
assert!(DatabaseType::MySQL.supports_window_functions());
assert!(DatabaseType::MariaDB.supports_window_functions());
assert!(DatabaseType::SQLite.supports_window_functions());
}
#[test]
fn test_database_type_supports_cte() {
assert!(DatabaseType::Postgres.supports_cte());
assert!(DatabaseType::MySQL.supports_cte());
assert!(DatabaseType::MariaDB.supports_cte());
assert!(DatabaseType::SQLite.supports_cte());
}
#[test]
fn test_database_type_supports_schemas() {
assert!(DatabaseType::Postgres.supports_schemas());
assert!(!DatabaseType::MySQL.supports_schemas());
assert!(!DatabaseType::MariaDB.supports_schemas());
assert!(!DatabaseType::SQLite.supports_schemas());
}
#[test]
fn test_database_type_optimal_batch_size() {
assert_eq!(DatabaseType::Postgres.optimal_batch_size(), 1000);
assert_eq!(DatabaseType::MySQL.optimal_batch_size(), 500);
assert_eq!(DatabaseType::MariaDB.optimal_batch_size(), 500);
assert_eq!(DatabaseType::SQLite.optimal_batch_size(), 100);
}
#[test]
fn test_database_type_param_style() {
assert_eq!(DatabaseType::Postgres.param_style(), "$");
assert_eq!(DatabaseType::MySQL.param_style(), "?");
assert_eq!(DatabaseType::MariaDB.param_style(), "?");
assert_eq!(DatabaseType::SQLite.param_style(), "?");
}
#[test]
fn test_database_type_quote_char() {
assert_eq!(DatabaseType::Postgres.quote_char(), '"');
assert_eq!(DatabaseType::MySQL.quote_char(), '`');
assert_eq!(DatabaseType::MariaDB.quote_char(), '`');
assert_eq!(DatabaseType::SQLite.quote_char(), '"');
}
#[test]
fn test_database_type_default_port() {
assert_eq!(DatabaseType::Postgres.default_port(), 5432);
assert_eq!(DatabaseType::MySQL.default_port(), 3306);
assert_eq!(DatabaseType::MariaDB.default_port(), 3306);
assert_eq!(DatabaseType::SQLite.default_port(), 0);
}
#[test]
fn test_database_type_url_scheme() {
assert_eq!(DatabaseType::Postgres.url_scheme(), "postgres");
assert_eq!(DatabaseType::MySQL.url_scheme(), "mysql");
assert_eq!(DatabaseType::MariaDB.url_scheme(), "mariadb");
assert_eq!(DatabaseType::SQLite.url_scheme(), "sqlite");
}
#[test]
fn test_database_type_display() {
assert_eq!(format!("{}", DatabaseType::Postgres), "PostgreSQL");
assert_eq!(format!("{}", DatabaseType::MySQL), "MySQL");
assert_eq!(format!("{}", DatabaseType::MariaDB), "MariaDB");
assert_eq!(format!("{}", DatabaseType::SQLite), "SQLite");
}
#[test]
fn test_database_type_is_mysql_compatible() {
assert!(!DatabaseType::Postgres.is_mysql_compatible());
assert!(DatabaseType::MySQL.is_mysql_compatible());
assert!(DatabaseType::MariaDB.is_mysql_compatible());
assert!(!DatabaseType::SQLite.is_mysql_compatible());
}
#[test]
fn test_tide_config_schema_file() {
let config = TideConfig::init()
.database_type(DatabaseType::Postgres)
.database("postgres://localhost/test")
.schema_file("test_schema.sql");
assert_eq!(config.schema_file, Some("test_schema.sql".to_string()));
}
#[test]
fn test_tide_config_schema_file_with_path() {
let config = TideConfig::init()
.database("postgres://localhost/test")
.schema_file("./database/schema.sql");
assert_eq!(config.schema_file, Some("./database/schema.sql".to_string()));
}
#[test]
fn test_tide_config_no_schema_file() {
let config = TideConfig::init()
.database("postgres://localhost/test");
assert!(config.schema_file.is_none());
}
#[test]
fn test_pool_config_defaults() {
let pool = PoolConfig::default();
assert_eq!(pool.max_connections, 10);
assert_eq!(pool.min_connections, 1);
assert_eq!(pool.connect_timeout, Duration::from_secs(8));
assert_eq!(pool.idle_timeout, Duration::from_secs(600));
assert_eq!(pool.max_lifetime, Duration::from_secs(1800));
assert_eq!(pool.acquire_timeout, Duration::from_secs(8));
}
#[test]
fn test_tide_config_full_chain() {
let config = TideConfig::init()
.database_type(DatabaseType::Postgres)
.database("postgres://localhost/test")
.max_connections(20)
.min_connections(5)
.connect_timeout(Duration::from_secs(10))
.idle_timeout(Duration::from_secs(300))
.max_lifetime(Duration::from_secs(3600))
.acquire_timeout(Duration::from_secs(5))
.schema_file("schema.sql")
.sync(false)
.languages(&["en", "fr", "ar"])
.fallback_language("en")
.hidden_attributes(&["password", "secret"]);
assert_eq!(config.database_type, Some(DatabaseType::Postgres));
assert_eq!(config.database_url, Some("postgres://localhost/test".to_string()));
assert_eq!(config.pool.max_connections, 20);
assert_eq!(config.pool.min_connections, 5);
assert_eq!(config.pool.connect_timeout, Duration::from_secs(10));
assert_eq!(config.pool.idle_timeout, Duration::from_secs(300));
assert_eq!(config.pool.max_lifetime, Duration::from_secs(3600));
assert_eq!(config.pool.acquire_timeout, Duration::from_secs(5));
assert_eq!(config.schema_file, Some("schema.sql".to_string()));
assert!(!config.sync_enabled);
assert_eq!(config.config.languages, vec!["en", "fr", "ar"]);
assert_eq!(config.config.fallback_language, "en");
assert_eq!(config.config.hidden_attributes, vec!["password", "secret"]);
}
}