rustorm-core 0.1.0

Core traits, types and utilities for RustORM
Documentation
//! Типы связей между моделями.
//!
//! Реализация хранит информацию о связи и предоставляет методы
//! для загрузки связанных записей.

use crate::error::OrmResult;
use crate::query::QueryBuilder;
use sqlx::PgPool;
use std::marker::PhantomData;

// ---------------------------------------------------------------------------
// BelongsTo<Related, FK>
// ---------------------------------------------------------------------------

/// Связь «принадлежит». Post belongs to User через `user_id`.
///
/// ```rust
/// type Author = BelongsTo<User, "user_id">;
/// let author = post.author().load(&pool).await?;
/// ```
pub struct BelongsToRef<'a, Owner, Related> {
    fk_value: i64,
    _owner: PhantomData<&'a Owner>,
    _related: PhantomData<Related>,
}

impl<'a, Owner, Related> BelongsToRef<'a, Owner, Related>
where
    Related: crate::model::Model + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
    pub fn new(fk_value: i64) -> Self {
        Self {
            fk_value,
            _owner: PhantomData,
            _related: PhantomData,
        }
    }

    pub async fn load(self, pool: &PgPool) -> OrmResult<Option<Related>> {
        Related::find(self.fk_value, pool).await
    }

    pub async fn load_or_fail(self, pool: &PgPool) -> OrmResult<Related> {
        Related::find_or_fail(self.fk_value, pool).await
    }
}

// ---------------------------------------------------------------------------
// HasMany<Related, FK>
// ---------------------------------------------------------------------------

/// Связь «имеет много». User has many Posts через `user_id`.
pub struct HasManyRef<'a, Owner, Related> {
    owner_id: i64,
    fk_col: &'static str,
    extra_filters: Vec<crate::column::FilterExpr>,
    _owner: PhantomData<&'a Owner>,
    _related: PhantomData<Related>,
}

impl<'a, Owner, Related> HasManyRef<'a, Owner, Related>
where
    Related: crate::model::Model
        + crate::query::HasColumns
        + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
        + Send
        + Sync
        + Unpin
        + 'static,
{
    pub fn new(owner_id: i64, fk_col: &'static str) -> Self {
        Self {
            owner_id,
            fk_col,
            extra_filters: vec![],
            _owner: PhantomData,
            _related: PhantomData,
        }
    }

    pub fn filter<F>(mut self, f: F) -> Self
    where
        F: FnOnce(&Related::Columns) -> crate::column::FilterExpr,
    {
        let cols = Related::columns_proxy();
        self.extra_filters.push(f(&cols));
        self
    }

    pub fn order_by<F>(self, _f: F) -> Self {
        self // TODO: forward to query builder
    }

    pub async fn load(self, pool: &PgPool) -> OrmResult<Vec<Related>> {
        let fk_filter = crate::column::FilterExpr::new(
            format!("\"{}\" = $1", self.fk_col),
            vec![crate::column::SqlValue::Int(self.owner_id)],
        );
        let mut qb = Related::query().filter_raw(fk_filter.sql);
        // Дополнительные фильтры
        for f in self.extra_filters {
            qb = qb.filter_raw(f.sql);
        }
        qb.fetch_all(pool).await
    }

    pub async fn count(self, pool: &PgPool) -> OrmResult<i64> {
        let fk_filter = crate::column::FilterExpr::new(
            format!("\"{}\" = $1", self.fk_col),
            vec![crate::column::SqlValue::Int(self.owner_id)],
        );
        Related::query().filter_raw(fk_filter.sql).count(pool).await
    }

    pub async fn create<N: serde::Serialize>(
        self,
        _new_record: N,
        _pool: &PgPool,
    ) -> OrmResult<Related> {
        // Генерируется конкретным кодом модели через макрос
        unimplemented!("Используйте Related::create()")
    }
}

// ---------------------------------------------------------------------------
// ManyToMany
// ---------------------------------------------------------------------------

/// Связь «многие ко многим» через pivot-таблицу.
pub struct ManyToManyRef<'a, Owner, Related> {
    owner_id: i64,
    pivot_table: &'static str,
    owner_fk: &'static str,
    related_fk: &'static str,
    _owner: PhantomData<&'a Owner>,
    _related: PhantomData<Related>,
}

impl<'a, Owner, Related> ManyToManyRef<'a, Owner, Related>
where
    Related: crate::model::Model
        + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
        + Send
        + Sync
        + Unpin
        + 'static,
{
    pub fn new(
        owner_id: i64,
        pivot_table: &'static str,
        owner_fk: &'static str,
        related_fk: &'static str,
    ) -> Self {
        Self {
            owner_id,
            pivot_table,
            owner_fk,
            related_fk,
            _owner: PhantomData,
            _related: PhantomData,
        }
    }

    pub async fn load(self, pool: &PgPool) -> OrmResult<Vec<Related>> {
        let sql = format!(
            r#"SELECT "{t}".* FROM "{t}"
               INNER JOIN "{pivot}" ON "{pivot}"."{rfk}" = "{t}"."{pk}"
               WHERE "{pivot}"."{ofk}" = $1"#,
            t = Related::table_name(),
            pivot = self.pivot_table,
            rfk = self.related_fk,
            pk = Related::primary_key(),
            ofk = self.owner_fk,
        );
        sqlx::query_as::<_, Related>(&sql)
            .bind(self.owner_id)
            .fetch_all(pool)
            .await
            .map_err(crate::error::OrmError::from_sqlx)
    }

    pub async fn attach(self, related_id: i64, pool: &PgPool) -> OrmResult<()> {
        let sql = format!(
            "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2) ON CONFLICT DO NOTHING",
            self.pivot_table, self.owner_fk, self.related_fk,
        );
        sqlx::query(&sql)
            .bind(self.owner_id)
            .bind(related_id)
            .execute(pool)
            .await
            .map_err(crate::error::OrmError::from_sqlx)?;
        Ok(())
    }

    pub async fn attach_many(self, related_ids: &[i64], pool: &PgPool) -> OrmResult<()> {
        for &id in related_ids {
            let sql = format!(
                "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2) ON CONFLICT DO NOTHING",
                self.pivot_table, self.owner_fk, self.related_fk,
            );
            sqlx::query(&sql)
                .bind(self.owner_id)
                .bind(id)
                .execute(pool)
                .await
                .map_err(crate::error::OrmError::from_sqlx)?;
        }
        Ok(())
    }

    pub async fn detach(self, related_id: i64, pool: &PgPool) -> OrmResult<()> {
        let sql = format!(
            "DELETE FROM \"{}\" WHERE \"{}\" = $1 AND \"{}\" = $2",
            self.pivot_table, self.owner_fk, self.related_fk,
        );
        sqlx::query(&sql)
            .bind(self.owner_id)
            .bind(related_id)
            .execute(pool)
            .await
            .map_err(crate::error::OrmError::from_sqlx)?;
        Ok(())
    }

    pub async fn detach_all(self, pool: &PgPool) -> OrmResult<()> {
        let sql = format!(
            "DELETE FROM \"{}\" WHERE \"{}\" = $1",
            self.pivot_table, self.owner_fk,
        );
        sqlx::query(&sql)
            .bind(self.owner_id)
            .execute(pool)
            .await
            .map_err(crate::error::OrmError::from_sqlx)?;
        Ok(())
    }

    pub async fn sync(self, related_ids: &[i64], pool: &PgPool) -> OrmResult<()> {
        let del_sql = format!(
            "DELETE FROM \"{}\" WHERE \"{}\" = $1",
            self.pivot_table, self.owner_fk,
        );
        sqlx::query(&del_sql)
            .bind(self.owner_id)
            .execute(pool)
            .await
            .map_err(crate::error::OrmError::from_sqlx)?;

        for &id in related_ids {
            let ins_sql = format!(
                "INSERT INTO \"{}\" (\"{}\", \"{}\") VALUES ($1, $2)",
                self.pivot_table, self.owner_fk, self.related_fk,
            );
            sqlx::query(&ins_sql)
                .bind(self.owner_id)
                .bind(id)
                .execute(pool)
                .await
                .map_err(crate::error::OrmError::from_sqlx)?;
        }
        Ok(())
    }
}