drizzle-core 0.3.0

A type-safe SQL query builder for Rust
Documentation
//! Shared `ON CONFLICT` builder machinery.

use core::marker::PhantomData;

use crate::expr::Expr;
use crate::prelude::Box;
use crate::sql::{SQL, Token};
use crate::traits::{SQLParam, ToSQL};
use crate::types::BooleanLike;

/// A conflict target that renders as `ON CONFLICT ...`.
pub trait ConflictTargetSql<'a, V: SQLParam> {
    /// Renders the target, followed by `WHERE target_where` when given.
    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V>;
}

/// Shared column-list conflict target: `ON CONFLICT (col1, col2)`.
#[derive(Debug, Clone)]
pub struct ConflictColumnsTarget<'a, V: SQLParam> {
    columns: SQL<'a, V>,
}

impl<'a, V: SQLParam> ConflictColumnsTarget<'a, V> {
    /// Creates a target from a comma-separated column list.
    #[inline]
    #[must_use]
    pub fn new(columns: SQL<'a, V>) -> Self {
        Self { columns }
    }
}

impl<'a, V: SQLParam> ConflictTargetSql<'a, V> for ConflictColumnsTarget<'a, V> {
    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V> {
        let mut target = SQL::from_iter([Token::ON, Token::CONFLICT, Token::LPAREN])
            .append(self.columns)
            .push(Token::RPAREN);
        if let Some(target_where) = target_where {
            target = target.push(Token::WHERE).append(target_where);
        }
        target
    }
}

/// PostgreSQL conflict target, including `ON CONSTRAINT`.
#[derive(Debug, Clone)]
pub enum PostgresConflictTarget<'a, V: SQLParam> {
    /// `ON CONFLICT (col1, col2)`.
    Columns(Box<ConflictColumnsTarget<'a, V>>),
    /// `ON CONFLICT ON CONSTRAINT "name"`. Takes no `WHERE`.
    Constraint(&'static str),
}

impl<'a, V: SQLParam> PostgresConflictTarget<'a, V> {
    /// Creates a column-list target.
    #[inline]
    #[must_use]
    pub fn columns(columns: SQL<'a, V>) -> Self {
        Self::Columns(Box::new(ConflictColumnsTarget::new(columns)))
    }

    /// Creates a named-constraint target.
    #[inline]
    #[must_use]
    pub const fn constraint(name: &'static str) -> Self {
        Self::Constraint(name)
    }
}

impl<'a, V: SQLParam> ConflictTargetSql<'a, V> for PostgresConflictTarget<'a, V> {
    fn into_target_sql(self, target_where: Option<SQL<'a, V>>) -> SQL<'a, V> {
        match self {
            Self::Columns(columns) => (*columns).into_target_sql(target_where),
            Self::Constraint(name) => {
                SQL::from_iter([Token::ON, Token::CONFLICT, Token::ON, Token::CONSTRAINT])
                    .append(SQL::ident(name))
            }
        }
    }
}

/// Builds the dialect's insert builder after an `ON CONFLICT` clause.
pub trait OnConflictOutput<'a, V: SQLParam, Schema, Table> {
    /// Builder returned by `.do_nothing()`.
    type OnConflictSet;
    /// Builder returned by `.do_update(...)`.
    type DoUpdateSet;

    /// Wraps the SQL so far in [`Self::OnConflictSet`].
    fn on_conflict(sql: SQL<'a, V>) -> Self::OnConflictSet;
    /// Wraps the SQL so far in [`Self::DoUpdateSet`].
    fn do_update(sql: SQL<'a, V>) -> Self::DoUpdateSet;
}

/// Builder for the `ON CONFLICT` clause of an INSERT, returned by the
/// dialect's `.on_conflict(...)`.
///
/// Finish it with [`do_nothing`](Self::do_nothing) or
/// [`do_update`](Self::do_update).
#[derive(Debug, Clone)]
pub struct OnConflictBuilder<'a, V, Schema, Table, Target, Output>
where
    V: SQLParam,
{
    sql: SQL<'a, V>,
    target: Target,
    target_where: Option<SQL<'a, V>>,
    schema: PhantomData<Schema>,
    table: PhantomData<Table>,
    output: PhantomData<Output>,
}

impl<'a, V, Schema, Table, Target, Output> OnConflictBuilder<'a, V, Schema, Table, Target, Output>
where
    V: SQLParam,
    Target: ConflictTargetSql<'a, V>,
    Output: OnConflictOutput<'a, V, Schema, Table>,
{
    /// Starts a conflict clause for the INSERT `sql` with the given target.
    #[inline]
    #[must_use]
    pub fn new(sql: SQL<'a, V>, target: Target) -> Self {
        Self {
            sql,
            target,
            target_where: None,
            schema: PhantomData,
            table: PhantomData,
            output: PhantomData,
        }
    }

    /// Seeds a partial-index predicate supplied by a typed conflict target.
    #[doc(hidden)]
    #[must_use]
    pub fn with_target_where_sql(mut self, target_where: Option<SQL<'a, V>>) -> Self {
        self.target_where = target_where;
        self
    }

    /// Adds a `WHERE` to the conflict target, to match a partial unique
    /// index.
    ///
    /// A typed partial-index target already supplies its declared predicate.
    /// Calling this replaces it, so the new predicate must still identify the
    /// same unique index.
    ///
    /// # Compile-time checks
    ///
    /// The condition must be boolean and may only read columns of the
    /// inserted table; reading another table fails with "is not in this
    /// query's FROM/JOIN scope".
    #[must_use]
    pub fn r#where<E, ScopeProof>(mut self, condition: E) -> Self
    where
        E: Expr<'a, V>,
        E::Sources: crate::scope::SourcesIn<crate::Cons<Table, crate::Nil>, ScopeProof>,
        E::SQLType: BooleanLike,
    {
        self.target_where = Some(condition.into_expr_sql());
        self
    }

    fn into_parts(self) -> (SQL<'a, V>, SQL<'a, V>) {
        (self.sql, self.target.into_target_sql(self.target_where))
    }

    /// Finishes the clause with `DO NOTHING`.
    #[must_use]
    pub fn do_nothing(self) -> Output::OnConflictSet {
        let (sql, target) = self.into_parts();
        Output::on_conflict(sql.append(target.push(Token::DO).push(Token::NOTHING)))
    }

    /// Finishes the clause with `DO UPDATE SET <set>`, updating the
    /// existing row.
    pub fn do_update(self, set: impl ToSQL<'a, V>) -> Output::DoUpdateSet {
        let (sql, target) = self.into_parts();
        let conflict = target
            .push(Token::DO)
            .push(Token::UPDATE)
            .push(Token::SET)
            .append(set.into_sql());
        Output::do_update(sql.append(conflict))
    }
}