use crate::Model;
#[derive(Model, Debug, Clone, serde::Serialize)]
#[rustango(table = "rustango_orgs", display = "slug", scope = "registry")]
#[allow(dead_code)] pub struct Org {
#[rustango(primary_key)]
pub id: rustango::Auto<i64>,
#[rustango(max_length = 64, unique)]
pub slug: String,
#[rustango(max_length = 255)]
pub display_name: String,
#[rustango(max_length = 16)]
pub storage_mode: String,
#[rustango(max_length = 16, default = "'postgres'")]
pub backend_kind: String,
pub database_url: Option<String>,
#[rustango(max_length = 64)]
pub schema_name: Option<String>,
#[rustango(max_length = 255)]
pub host_pattern: Option<String>,
pub port: Option<i32>,
#[rustango(max_length = 255)]
pub path_prefix: Option<String>,
pub active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
#[rustango(max_length = 80)]
pub brand_name: Option<String>,
#[rustango(max_length = 200)]
pub brand_tagline: Option<String>,
#[rustango(max_length = 120)]
pub logo_path: Option<String>,
#[rustango(max_length = 120)]
pub favicon_path: Option<String>,
#[rustango(max_length = 7)]
pub primary_color: Option<String>,
#[rustango(max_length = 8)]
pub theme_mode: Option<String>,
}
impl Org {
pub(crate) fn effective_schema(&self) -> &str {
effective_schema(self.schema_name.as_deref(), &self.slug)
}
}
pub(crate) fn effective_schema<'a>(schema_name: Option<&'a str>, slug: &'a str) -> &'a str {
schema_name.unwrap_or(slug)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageMode {
Schema,
Database,
}
impl StorageMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Schema => "schema",
Self::Database => "database",
}
}
pub fn parse(s: &str) -> Result<Self, &str> {
match s {
"schema" => Ok(Self::Schema),
"database" => Ok(Self::Database),
other => Err(other),
}
}
}
impl core::fmt::Display for StorageMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackendKind {
#[default]
Postgres,
MySql,
Sqlite,
}
impl BackendKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Postgres => "postgres",
Self::MySql => "mysql",
Self::Sqlite => "sqlite",
}
}
pub fn parse(s: &str) -> Result<Self, &str> {
match s {
"postgres" | "postgresql" | "pg" => Ok(Self::Postgres),
"mysql" | "mariadb" => Ok(Self::MySql),
"sqlite" | "sqlite3" => Ok(Self::Sqlite),
other => Err(other),
}
}
pub const fn validate_storage_mode(self, mode: StorageMode) -> Result<(), &'static str> {
match (self, mode) {
(Self::Postgres, _) => Ok(()),
(_, StorageMode::Database) => Ok(()),
(Self::MySql, StorageMode::Schema) => {
Err("MySQL does not support schema mode — choose the database storage mode")
}
(Self::Sqlite, StorageMode::Schema) => Err(
"SQLite does not support schema mode — choose the database storage mode, \
which gives each tenant its own file",
),
}
}
}
impl core::fmt::Display for BackendKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrgGeneration {
pub count: i64,
pub active: i64,
pub max_id: i64,
pub active_id_sum: i64,
}
pub async fn generation(
registry: &crate::sql::Pool,
) -> Result<OrgGeneration, crate::sql::ExecError> {
use crate::core::aggregates::{count_all, max, sum};
use crate::core::{AggregateQuery, Column as _, Model as _, WhereExpr};
use crate::sql::fetch_aggregate_pool;
let q = AggregateQuery {
model: Org::SCHEMA,
joins: Vec::new(),
where_clause: WhereExpr::And(Vec::new()),
group_by: Vec::new(),
aggregates: vec![
("n".into(), count_all().into()),
(
"n_on".into(),
count_all().filter(Org::active.eq(true)).into(),
),
("max_id".into(), max("id").into()),
(
"on_id_sum".into(),
sum("id").filter(Org::active.eq(true)).into(),
),
],
aliases: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
source: None,
};
let rows: Vec<(Option<i64>, Option<i64>, Option<i64>, Option<i64>)> =
fetch_aggregate_pool(registry, &q).await?;
let (n, n_on, max_id, on_id_sum) = rows.into_iter().next().unwrap_or((None, None, None, None));
Ok(OrgGeneration {
count: n.unwrap_or(0),
active: n_on.unwrap_or(0),
max_id: max_id.unwrap_or(0),
active_id_sum: on_id_sum.unwrap_or(0),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accepts_aliases() {
assert_eq!(
BackendKind::parse("postgres").unwrap(),
BackendKind::Postgres
);
assert_eq!(
BackendKind::parse("postgresql").unwrap(),
BackendKind::Postgres
);
assert_eq!(BackendKind::parse("pg").unwrap(), BackendKind::Postgres);
assert_eq!(BackendKind::parse("mysql").unwrap(), BackendKind::MySql);
assert_eq!(BackendKind::parse("mariadb").unwrap(), BackendKind::MySql);
assert_eq!(BackendKind::parse("sqlite").unwrap(), BackendKind::Sqlite);
assert_eq!(BackendKind::parse("sqlite3").unwrap(), BackendKind::Sqlite);
}
#[test]
fn parse_rejects_unknown() {
assert!(BackendKind::parse("oracle").is_err());
assert!(BackendKind::parse("").is_err());
}
#[test]
fn validate_storage_mode_postgres_accepts_both() {
assert!(BackendKind::Postgres
.validate_storage_mode(StorageMode::Schema)
.is_ok());
assert!(BackendKind::Postgres
.validate_storage_mode(StorageMode::Database)
.is_ok());
}
#[test]
fn validate_storage_mode_mysql_rejects_schema() {
let err = BackendKind::MySql
.validate_storage_mode(StorageMode::Schema)
.unwrap_err();
assert!(err.contains("MySQL"));
assert_operator_facing(err);
}
fn assert_operator_facing(msg: &str) {
assert!(!msg.contains("storage_mode="), "wire value: {msg}");
assert!(!msg.contains("--"), "CLI flag: {msg}");
assert!(msg.contains("database storage mode"), "{msg}");
}
#[test]
fn validate_storage_mode_sqlite_rejects_schema() {
let err = BackendKind::Sqlite
.validate_storage_mode(StorageMode::Schema)
.unwrap_err();
assert!(err.contains("SQLite"));
assert_operator_facing(err);
}
#[test]
fn non_postgres_accepts_database_mode() {
assert!(BackendKind::MySql
.validate_storage_mode(StorageMode::Database)
.is_ok());
assert!(BackendKind::Sqlite
.validate_storage_mode(StorageMode::Database)
.is_ok());
}
#[test]
fn default_is_postgres() {
assert_eq!(BackendKind::default(), BackendKind::Postgres);
}
}