mool 0.2.0

A source-first Rust database toolkit for typed SQL queries, migrations, and model metadata
Documentation
#![allow(dead_code)]

use std::borrow::Cow;

use mool as db;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy)]
pub struct BindMeta {
    pub prebound: usize,
    pub dynamic: usize,
    pub total: usize,
}

impl BindMeta {
    pub const fn new(prebound: usize, dynamic: usize, total: usize) -> Self {
        Self {
            prebound,
            dynamic,
            total,
        }
    }
}

pub fn assert_plan(plan: &db::QueryPlan, expected_sql: &str, expected_binds: BindMeta) {
    assert_eq!(plan.sql, expected_sql);
    assert_plan_meta(plan, expected_binds);
}

pub fn assert_plan_meta(plan: &db::QueryPlan, expected: BindMeta) {
    assert_eq!(plan.prebound_count, expected.prebound);
    assert_eq!(plan.dynamic_bind_count, expected.dynamic);
    assert_eq!(plan.total_bind_count, expected.total);
    assert_eq!(
        plan.prebound_count + plan.dynamic_bind_count,
        plan.total_bind_count
    );
}

pub fn assert_param(
    plan: &db::QueryPlan,
    name: &str,
    source: db::queries::ParamSource,
    position: usize,
    occurrences: &[usize],
) {
    let param = plan
        .params
        .get(name)
        .unwrap_or_else(|| panic!("missing parameter {name} in {:?}", plan.params.keys()));
    assert_eq!(param.source, source);
    assert_eq!(param.position, position);
    assert_eq!(param.occurrences, occurrences);
}

pub fn assert_unsupported<T>(result: Result<T, db::QueryError>, needle: &str)
where
    T: std::fmt::Debug,
{
    let err = result.expect_err("expected query planning to fail");
    assert!(
        err.to_string().contains(needle),
        "expected error to contain {needle:?}, got {err}"
    );
}

pub fn col<'a>(table: &'a db::Table, name: &str) -> &'a db::Column {
    table
        .columns
        .iter()
        .find(|column| column.name == name)
        .unwrap_or_else(|| panic!("missing column {name} on {}", table.name))
}

pub fn table<'a>(schema: &'a db::Schema, name: &str) -> &'a db::Table {
    schema
        .tables
        .get(name)
        .unwrap_or_else(|| panic!("missing table {name}"))
}

#[derive(Debug, Clone, PartialEq, db::Model)]
#[table(name = "users")]
pub struct User {
    #[column(primary_key)]
    pub id: i64,
    #[column(unique, index)]
    pub email: String,
    pub active: bool,
}

#[derive(Debug, Clone, PartialEq, db::Model)]
#[table(name = "posts")]
pub struct Post {
    pub id: i64,
    #[column(reference(target = "users.id", name = "posts_author_id_fkey"))]
    pub author_id: i64,
    pub title: String,
    pub published: bool,
    #[column(type = "timestamptz")]
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[column(nullable)]
    pub subtitle: Option<String>,
}

#[derive(Debug, Clone, PartialEq, db::Model)]
#[table(name = "comments")]
pub struct Comment {
    pub id: i64,
    #[column(reference = "posts.id")]
    pub post_id: i64,
    pub body: String,
    pub flagged: bool,
}

#[derive(Debug, Clone, PartialEq, db::Model)]
#[table(name = "tags")]
pub struct Tag {
    pub id: i64,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, db::Model)]
#[table(name = "post_tags")]
pub struct PostTag {
    pub id: i64,
    #[column(reference = "posts.id")]
    pub post_id: i64,
    #[column(reference = "tags.id")]
    pub tag_id: i64,
}

#[derive(Debug, Clone, db::Model)]
#[table(name = "accounts", schema = "auth")]
pub struct Account {
    #[column(primary_key)]
    pub id: i64,
    #[column(name = "email_address", type = "citext")]
    pub email: String,
    #[column(nullable)]
    pub nickname: Option<String>,
}

#[derive(Debug, Clone, db::Model)]
#[table(
    name = "memberships",
    primary_key(name = "memberships_identity", columns = ["tenant_id", "user_id"])
)]
pub struct Membership {
    pub tenant_id: i64,
    pub user_id: i64,
    pub role: String,
}

#[derive(Debug, Clone, db::Model)]
pub struct AuditLog {
    pub id: i64,
    pub message: String,
}

#[derive(Debug, Clone, db::Record)]
pub struct PostWithAuthor {
    #[column(flatten)]
    pub post: Post,
    #[column(reference(on(from = "author_id", to = "id")))]
    pub author: User,
}

#[derive(Debug, Clone, db::Record)]
#[table(name = "posts")]
pub struct PostSummary {
    pub id: i64,
    pub title: String,
}

#[derive(Debug, Clone, db::Record)]
#[table(name = "posts")]
pub struct PostPatch {
    pub title: String,
    pub published: bool,
}

#[derive(Debug, Clone, db::Record)]
#[table(name = "posts")]
pub struct PostTitlePatch {
    pub title: String,
}

#[derive(Debug, Clone, db::Record)]
#[table(name = "posts")]
pub struct PostStats {
    pub author_id: i64,
    pub post_count: i64,
    pub avg_id: f64,
}

#[derive(Debug, Clone, db::Record)]
#[table(name = "posts")]
pub struct RankedPost {
    pub id: i64,
    pub row_number: i64,
    pub rank: i64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, db::SqlEnum)]
#[sql_enum(rename_all = "snake_case")]
pub enum PostStatus {
    Draft,
    InReview,
    Published,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, db::SqlEnum)]
#[sql_enum(storage = "int", repr = "i16")]
pub enum PostPriority {
    #[sql_enum(code = 1)]
    Low,
    #[sql_enum(code = 2)]
    High,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, db::SqlEnum)]
#[sql_enum(name = "native_post_status", storage = "native_postgres")]
pub enum NativePostStatus {
    #[sql_enum(value = "draft")]
    Draft,
    #[sql_enum(value = "published")]
    Published,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, db::SqlEnum)]
#[sql_enum(storage = "native_mysql", rename_all = "snake_case")]
pub enum MysqlPostStatus {
    Draft,
    Published,
}

#[derive(Debug, Clone, db::Model)]
#[table(name = "enum_posts")]
pub struct EnumPost {
    pub id: i64,
    #[column(sql_enum)]
    pub status: PostStatus,
    #[column(sql_enum)]
    pub priority: PostPriority,
}

#[derive(Debug, Clone, db::Model)]
#[table(name = "native_enum_posts")]
pub struct NativeEnumPost {
    pub id: i64,
    #[column(sql_enum)]
    pub status: NativePostStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostMeta {
    pub status: String,
}

#[derive(Debug, Clone, db::Model)]
#[table(name = "json_posts")]
pub struct JsonPost {
    pub id: i64,
    #[column(type = "jsonb")]
    pub meta: PostMeta,
}

#[cfg(feature = "postgres")]
#[derive(Debug, Clone, db::Model)]
#[table(name = "array_posts")]
pub struct ArrayPost {
    pub id: i64,
    pub tags: Vec<String>,
    pub scores: Option<Vec<i64>>,
}

#[derive(Debug, Clone, db::Filterable)]
#[filter(model = Post)]
pub struct PostFilter {
    #[filter(op = "eq")]
    pub published: Option<bool>,
    #[filter(op = "ilike", column = "title")]
    pub q: Option<String>,
    #[filter(op = "in", column = "id")]
    pub ids: Vec<i64>,
}

#[derive(Debug, Clone, db::Filterable)]
#[filter(model = EnumPost)]
pub struct EnumPostFilter {
    #[filter(op = "eq")]
    pub status: Option<PostStatus>,
    #[filter(op = "in")]
    pub priority: Vec<PostPriority>,
}

pub struct PostComments;

impl db::Backref for PostComments {
    type From = Post;
    type To = Comment;

    const NAME: &'static str = "comments";
    const CARDINALITY: db::RelationCardinality = db::RelationCardinality::Many;

    fn meta() -> db::ReferenceMeta {
        db::ReferenceMeta {
            logical_name: "comment",
            table_name: "comments",
            table_schema: None,
            columns: &[db::JoinColumn {
                from: "post.id",
                to: "post_id",
            }],
            join_type: db::JoinType::Inner,
        }
    }
}

impl db::ManyBackref for PostComments {}

pub struct PostTags;

impl db::ManyToMany for PostTags {
    type From = Post;
    type Through = PostTag;
    type To = Tag;

    const NAME: &'static str = "tags";

    fn from_through() -> db::ReferenceMeta {
        db::ReferenceMeta {
            logical_name: "post_tag",
            table_name: "post_tags",
            table_schema: None,
            columns: &[db::JoinColumn {
                from: "post.id",
                to: "post_id",
            }],
            join_type: db::JoinType::Inner,
        }
    }

    fn through_to() -> db::ReferenceMeta {
        db::ReferenceMeta {
            logical_name: "tag",
            table_name: "tags",
            table_schema: None,
            columns: &[db::JoinColumn {
                from: "tag_id",
                to: "id",
            }],
            join_type: db::JoinType::Inner,
        }
    }
}

#[derive(Clone)]
pub struct LowerTitle {
    pub title: db::queries::Column<String>,
}

impl db::DbExpression<String> for LowerTitle {
    fn args(&self) -> db::FunctionArgs {
        db::FunctionArgs::new((&self.title,))
    }

    fn render(&self, ctx: &mut db::ExprRenderCtx<'_>) -> Result<String, db::QueryError> {
        Ok(format!("LOWER({})", ctx.arg(0)?))
    }
}

#[derive(Clone)]
pub struct SearchRank;

impl db::DbFunction<f64> for SearchRank {
    fn name(&self, _dialect: db::queries::Dialect) -> Result<Cow<'static, str>, db::QueryError> {
        Ok(Cow::Borrowed("search_rank"))
    }
}