arcature-data 2026.2.0

Arcature high-level data layer: explicit-ownership model/query ergonomics over SeaORM/SQLx, N+1 detection, and migration lint.
Documentation
//! The explicit-ownership typed query builder over SeaORM entities.
//!
//! [`Query<E>`] borrows a [`Db`] and wraps a SeaORM `Select<E>`. The golden
//! path reaches it via the blanket [`QueryModel`] trait: any SeaORM
//! `Entity` gains `Entity::query(&db)` with **no application-side trait
//! implementation** — the blanket `impl<E: EntityTrait> QueryModel for E`
//! covers every entity (PROGRAM.md AP2.1-6: "choose by actual Rust ergonomics").
//!
//! # Explicit ownership — no hidden global
//!
//! Every terminal method (`.all()`, `.one()`) resolves the query against the
//! `&Db` the builder already holds. The caller never re-passes `db.orm()` on
//! each terminal call, and there is no thread-local, task-local, or
//! request-global pool the framework resolves on the caller's behalf
//! (AGENTS.md §20; PROGRAM.md AP2.1-6).
//!
//! # What this does not own
//!
//! `Query<E>` does not reimplement SeaORM's query builder, condition engine,
//! or relation engine. It delegates to SeaORM's `Select<E>` for filter, order,
//! limit, offset, and pagination, and to `Entity::find_by_id` /
//! `find_also_related` for primary-key lookup and eager loading. Raw SeaORM
//! remains a first-class escape hatch (`db.orm()`).

use arcature_db::Db;
use arcature_db::sea_orm;
use arcature_db::sea_orm::sea_query::Order;
use arcature_db::sea_orm::{QueryFilter, QueryOrder, QuerySelect};

use crate::error::DataError;

use super::paginate::Paginated;

/// A typed query bound to an explicit `&Db`, over a SeaORM entity `E`.
///
/// Construct on the golden path with [`QueryModel::query`]:
///
/// ```ignore
/// use arcature_data::QueryModel;
/// # async fn run(db: &arcature_db::Db) -> Result<(), arcature_data::DataError> {
/// let posts = post::Entity::query(db)
///     .filter(post::Column::UserId.eq(7))
///     .order_by_desc(post::Column::CreatedAt)
///     .all()
///     .await?;
/// # Ok(())
/// # }
/// ```
///
/// Or construct directly with [`Query::new`]. The builder carries the `&Db`
/// for the lifetime of the query, so terminal methods need no extra argument.
pub struct Query<'db, E>
where
    E: sea_orm::EntityTrait,
{
    pub(crate) db: &'db Db,
    pub(crate) select: sea_orm::Select<E>,
}

impl<'db, E> Query<'db, E>
where
    E: sea_orm::EntityTrait,
{
    /// Construct a query over all rows of `E`, bound to `db`.
    #[must_use]
    pub fn new(db: &'db Db) -> Self {
        Self {
            db,
            select: E::find(),
        }
    }

    /// Add a filter condition. Accepts anything SeaORM's `Select::filter`
    /// accepts — typically `Column::X.eq(v)` or a built `Condition`.
    ///
    /// ```ignore
    /// post::Entity::query(db).filter(post::Column::Active.eq(true))
    /// ```
    #[must_use]
    pub fn filter<C>(mut self, condition: C) -> Self
    where
        C: sea_orm::sea_query::IntoCondition,
    {
        self.select = self.select.filter(condition);
        self
    }

    /// Add an `ORDER BY` clause on a column with an explicit direction.
    #[must_use]
    pub fn order_by<C>(mut self, column: C, order: Order) -> Self
    where
        C: sea_orm::IntoSimpleExpr,
    {
        self.select = self.select.order_by(column, order);
        self
    }

    /// Add an ascending `ORDER BY` clause on a column.
    #[must_use]
    pub fn order_by_asc<C>(mut self, column: C) -> Self
    where
        C: sea_orm::IntoSimpleExpr,
    {
        self.select = self.select.order_by_asc(column);
        self
    }

    /// Add a descending `ORDER BY` clause on a column.
    #[must_use]
    pub fn order_by_desc<C>(mut self, column: C) -> Self
    where
        C: sea_orm::IntoSimpleExpr,
    {
        self.select = self.select.order_by_desc(column);
        self
    }

    /// Limit the number of rows returned.
    #[must_use]
    pub fn limit(mut self, n: u64) -> Self {
        self.select = self.select.limit(n);
        self
    }

    /// Skip the first `n` rows.
    #[must_use]
    pub fn offset(mut self, n: u64) -> Self {
        self.select = self.select.offset(n);
        self
    }

    /// Begin pagination with the given per-page size (must be >= 1). Chain
    /// `.page(n)` (1-based) and then `.fetch()` to run the count + page query.
    #[must_use]
    pub fn paginate(self, per_page: u64) -> Paginated<'db, E> {
        Paginated::new(self.db, self.select, per_page)
    }

    /// Execute the query and return all matching rows.
    ///
    /// # Errors
    ///
    /// Returns [`DataError::Database`] if the underlying SeaORM query fails.
    pub async fn all(self) -> Result<Vec<E::Model>, DataError> {
        self.select
            .all(self.db.orm())
            .await
            .map_err(DataError::from)
    }

    /// Execute the query and return the first matching row, if any.
    ///
    /// # Errors
    ///
    /// Returns [`DataError::Database`] if the underlying SeaORM query fails.
    pub async fn one(self) -> Result<Option<E::Model>, DataError> {
        self.select
            .one(self.db.orm())
            .await
            .map_err(DataError::from)
    }
}

/// The golden-path entry point: every SeaORM `Entity` gains `Entity::query(&db)`.
///
/// This is a blanket implementation over [`sea_orm::EntityTrait`] — applications
/// do **not** implement it by hand, and no `#[model]` macro is required
/// (PROGRAM.md AP2.1-6; the macro is deferred to a later AP2.1 phase). The
/// trait adds the single `query` associated function; all other ergonomics
/// live on [`Query<E>`] and the free functions [`crate::find_by_pk`],
/// [`crate::insert`], [`crate::update`], [`crate::delete`].
pub trait QueryModel: sea_orm::EntityTrait {
    /// Start a typed query over this entity, bound to the explicit `db`.
    #[must_use]
    fn query(db: &Db) -> Query<'_, Self> {
        Query::new(db)
    }
}

impl<E> QueryModel for E where E: sea_orm::EntityTrait {}