#![allow(clippy::manual_async_fn)]
use crate::DjogiError;
use crate::context::DjogiContext;
use crate::expr::Expr;
use crate::expr::arithmetic::Numeric;
use crate::expr::node::{CmpOp, ExprNode};
use crate::model::Model;
use crate::pg::accumulator::as_params;
use crate::pg::decode::FromPgRow;
use crate::query::field::{DjogiField, FieldRef, IntoSqlField};
use crate::query::queryset::QuerySet;
use crate::query::sql::{
build_insert_select_returning_with_conflict, build_insert_select_with_conflict,
};
use crate::query::terminal::auto_set_tenant;
use std::collections::HashSet;
use std::future::Future;
use std::marker::PhantomData;
#[must_use = "InsertSelectSource is lazy — drop one and the source projection is silently omitted"]
#[derive(Debug, Clone)]
pub struct InsertSelectSource<S: Model, V> {
pub(crate) node: ExprNode,
_phantom: PhantomData<fn() -> (S, V)>,
}
impl<S: Model, V> InsertSelectSource<S, V> {
#[must_use = "InsertSelectSource is lazy — drop one and the source projection is silently omitted"]
pub fn literal(v: V) -> Self
where
V: Into<Expr<V>>,
{
let expr: Expr<V> = v.into();
Self {
node: expr.node,
_phantom: PhantomData,
}
}
pub(crate) fn from_node(node: ExprNode) -> Self {
Self {
node,
_phantom: PhantomData,
}
}
}
impl<S: Model, V: Numeric> std::ops::Add for InsertSelectSource<S, V> {
type Output = InsertSelectSource<S, V>;
fn add(self, rhs: Self) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Add(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model, V: Numeric> std::ops::Sub for InsertSelectSource<S, V> {
type Output = InsertSelectSource<S, V>;
fn sub(self, rhs: Self) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Sub(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model, V: Numeric> std::ops::Mul for InsertSelectSource<S, V> {
type Output = InsertSelectSource<S, V>;
fn mul(self, rhs: Self) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Mul(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model, V: Numeric> std::ops::Div for InsertSelectSource<S, V> {
type Output = InsertSelectSource<S, V>;
fn div(self, rhs: Self) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Div(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model> std::ops::Add<InsertSelectSource<S, time::Duration>>
for InsertSelectSource<S, time::OffsetDateTime>
{
type Output = InsertSelectSource<S, time::OffsetDateTime>;
fn add(self, rhs: InsertSelectSource<S, time::Duration>) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Add(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model> std::ops::Sub<InsertSelectSource<S, time::Duration>>
for InsertSelectSource<S, time::OffsetDateTime>
{
type Output = InsertSelectSource<S, time::OffsetDateTime>;
fn sub(self, rhs: InsertSelectSource<S, time::Duration>) -> Self::Output {
InsertSelectSource::from_node(ExprNode::Sub(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<S: Model, V> FieldRef<S, V> {
#[must_use = "InsertSelectSource is lazy — drop one and the source projection is silently omitted"]
pub fn as_insert_source(self) -> InsertSelectSource<S, V> {
InsertSelectSource::from_node(ExprNode::Field {
column: self.column(),
})
}
}
impl<T: Model, V> FieldRef<T, V> {
#[must_use = "an ExcludedRef is lazy — use it in a DO UPDATE SET assignment or a condition"]
pub fn excluded(self) -> ExcludedRef<T, V> {
ExcludedRef::from_node(ExprNode::Excluded {
column: self.column(),
})
}
#[must_use = "a ConflictExpr is lazy — use it in a DO UPDATE SET assignment"]
pub fn as_conflict_expr(self) -> ConflictExpr<T, V> {
ConflictExpr::from_node(ExprNode::Field {
column: self.column(),
})
}
}
#[must_use = "column mappings are lazy — drop one and the INSERT silently omits the column"]
pub struct InsertSelectColumn<S: Model, T: Model> {
pub(crate) target_column: &'static str,
pub(crate) source: ExprNode,
_phantom: PhantomData<fn() -> (S, T)>,
}
impl<S: Model, T: Model> std::fmt::Debug for InsertSelectColumn<S, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InsertSelectColumn")
.field("source_table", &S::table_name())
.field("target_table", &T::table_name())
.field("target_column", &self.target_column)
.field("source", &self.source)
.finish()
}
}
impl<S: Model, T: Model> Clone for InsertSelectColumn<S, T> {
fn clone(&self) -> Self {
InsertSelectColumn {
target_column: self.target_column,
source: self.source.clone(),
_phantom: PhantomData,
}
}
}
impl<S: Model, T: Model> InsertSelectColumn<S, T> {
#[doc(hidden)]
pub fn target_column(&self) -> &'static str {
self.target_column
}
#[doc(hidden)]
pub(crate) fn source(&self) -> &ExprNode {
&self.source
}
}
impl<T: Model, V> FieldRef<T, V> {
#[must_use = "column mappings are lazy — drop one and the INSERT silently omits the column"]
pub fn copy_from<S: Model>(self, source: InsertSelectSource<S, V>) -> InsertSelectColumn<S, T> {
InsertSelectColumn {
target_column: self.column(),
source: source.node,
_phantom: PhantomData,
}
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_set<S: Model>(self, value: ExcludedRef<T, V>) -> ConflictUpdate<S, T> {
ConflictUpdate {
target_column: self.column(),
value: ConflictUpdateValue { node: value.node },
_phantom: PhantomData,
}
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_set_expr<S: Model, E>(self, value: E) -> ConflictUpdate<S, T>
where
E: IntoConflictExpr<T, V>,
{
ConflictUpdate {
target_column: self.column(),
value: ConflictUpdateValue {
node: value.into_conflict_expr().node,
},
_phantom: PhantomData,
}
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_set_value<S: Model>(self, value: V) -> ConflictUpdate<S, T>
where
V: Into<Expr<V>>,
{
let expr: Expr<V> = value.into();
ConflictUpdate {
target_column: self.column(),
value: ConflictUpdateValue { node: expr.node },
_phantom: PhantomData,
}
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_excluded<S: Model>(self) -> ConflictUpdate<S, T> {
let column = self.column();
ConflictUpdate {
target_column: column,
value: ConflictUpdateValue {
node: ExprNode::Excluded { column },
},
_phantom: PhantomData,
}
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_add<S: Model>(self, value: V) -> ConflictUpdate<S, T>
where
V: Numeric + Into<Expr<V>>,
{
self.conflict_arith::<S>(value, ExprNode::Add)
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_sub<S: Model>(self, value: V) -> ConflictUpdate<S, T>
where
V: Numeric + Into<Expr<V>>,
{
self.conflict_arith::<S>(value, ExprNode::Sub)
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_mul<S: Model>(self, value: V) -> ConflictUpdate<S, T>
where
V: Numeric + Into<Expr<V>>,
{
self.conflict_arith::<S>(value, ExprNode::Mul)
}
#[must_use = "a ConflictUpdate is lazy — drop one and DO UPDATE SET silently omits the column"]
pub fn conflict_div<S: Model>(self, value: V) -> ConflictUpdate<S, T>
where
V: Numeric + Into<Expr<V>>,
{
self.conflict_arith::<S>(value, ExprNode::Div)
}
fn conflict_arith<S: Model>(
self,
value: V,
op: fn(Box<ExprNode>, Box<ExprNode>) -> ExprNode,
) -> ConflictUpdate<S, T>
where
V: Numeric + Into<Expr<V>>,
{
let column = self.column();
let rhs: Expr<V> = value.into();
ConflictUpdate {
target_column: column,
value: ConflictUpdateValue {
node: op(Box::new(ExprNode::Field { column }), Box::new(rhs.node)),
},
_phantom: PhantomData,
}
}
pub fn conflict_eq<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Eq,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_neq<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Neq,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_gt<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Gt,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_gte<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Gte,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_lt<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Lt,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_lte<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Lte,
rhs.into_conflict_expr().node,
)
}
pub fn conflict_eq_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Eq,
e.node,
)
}
pub fn conflict_neq_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Neq,
e.node,
)
}
pub fn conflict_gt_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Gt,
e.node,
)
}
pub fn conflict_gte_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Gte,
e.node,
)
}
pub fn conflict_lt_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Lt,
e.node,
)
}
pub fn conflict_lte_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(
ExprNode::Field {
column: self.column(),
},
CmpOp::Lte,
e.node,
)
}
}
impl<T: Model> FieldRef<T, bool> {
pub fn conflict_is_true(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::Field {
column: self.column(),
},
_marker: PhantomData,
}
}
pub fn conflict_is_false(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::Not(Box::new(ExprNode::Field {
column: self.column(),
})),
_marker: PhantomData,
}
}
}
impl<T: Model, V> FieldRef<T, Option<V>> {
pub fn conflict_is_null(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::IsNull(Box::new(ExprNode::Field {
column: self.column(),
})),
_marker: PhantomData,
}
}
pub fn conflict_is_not_null(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::IsNotNull(Box::new(ExprNode::Field {
column: self.column(),
})),
_marker: PhantomData,
}
}
#[must_use = "a ConflictExpr is lazy — use it in a DO UPDATE SET assignment"]
pub fn conflict_coalesce_excluded(self) -> ConflictExpr<T, Option<V>> {
ConflictExpr::from_node(ExprNode::Coalesce(vec![
ExprNode::Field {
column: self.column(),
},
ExprNode::Excluded {
column: self.column(),
},
]))
}
}
impl<T: Model, V> ExcludedRef<T, V> {
pub fn conflict_eq<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Eq, rhs.into_conflict_expr().node)
}
pub fn conflict_neq<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Neq, rhs.into_conflict_expr().node)
}
pub fn conflict_gt<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Gt, rhs.into_conflict_expr().node)
}
pub fn conflict_gte<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Gte, rhs.into_conflict_expr().node)
}
pub fn conflict_lt<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Lt, rhs.into_conflict_expr().node)
}
pub fn conflict_lte<E: IntoConflictExpr<T, V>>(self, rhs: E) -> ConflictCondition<T> {
conflict_condition(self.node, CmpOp::Lte, rhs.into_conflict_expr().node)
}
pub fn conflict_eq_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Eq, e.node)
}
pub fn conflict_neq_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Neq, e.node)
}
pub fn conflict_gt_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Gt, e.node)
}
pub fn conflict_gte_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Gte, e.node)
}
pub fn conflict_lt_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Lt, e.node)
}
pub fn conflict_lte_value(self, value: V) -> ConflictCondition<T>
where
V: Into<Expr<V>>,
{
let e: Expr<V> = value.into();
conflict_condition(self.node, CmpOp::Lte, e.node)
}
}
impl<T: Model, V> ExcludedRef<T, Option<V>> {
pub fn conflict_is_null(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::IsNull(Box::new(self.node)),
_marker: PhantomData,
}
}
pub fn conflict_is_not_null(self) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::IsNotNull(Box::new(self.node)),
_marker: PhantomData,
}
}
}
pub trait IntoInsertColumns<S: Model, T: Model> {
fn into_insert_columns(self) -> Vec<InsertSelectColumn<S, T>>;
}
impl<S: Model, T: Model> IntoInsertColumns<S, T> for InsertSelectColumn<S, T> {
fn into_insert_columns(self) -> Vec<InsertSelectColumn<S, T>> {
vec![self]
}
}
impl<S: Model, T: Model> IntoInsertColumns<S, T> for Vec<InsertSelectColumn<S, T>> {
fn into_insert_columns(self) -> Vec<InsertSelectColumn<S, T>> {
self
}
}
mod __conflict_sealed {
pub trait Sealed {}
}
#[must_use = "an OnConflictClause is inert until attached to an InsertSelectStmt and executed"]
pub struct OnConflictClause<S: Model, T: Model> {
pub(crate) target: Option<ConflictTarget<T>>,
pub(crate) action: ConflictAction<S, T>,
}
impl<S: Model, T: Model> Clone for OnConflictClause<S, T> {
fn clone(&self) -> Self {
Self {
target: self.target.clone(),
action: self.action.clone(),
}
}
}
impl<S: Model, T: Model> std::fmt::Debug for OnConflictClause<S, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnConflictClause")
.field("target", &self.target)
.field("action", &self.action)
.finish()
}
}
#[non_exhaustive]
pub enum ConflictTarget<T: Model> {
#[non_exhaustive]
Columns {
columns: Vec<&'static str>,
inference_predicate: Option<Box<ConflictCondition<T>>>,
},
#[non_exhaustive]
Constraint {
name: &'static str,
inference_predicate: Option<Box<ConflictCondition<T>>>,
},
}
impl<T: Model> Clone for ConflictTarget<T> {
fn clone(&self) -> Self {
match self {
Self::Columns {
columns,
inference_predicate,
} => Self::Columns {
columns: columns.clone(),
inference_predicate: inference_predicate.clone(),
},
Self::Constraint {
name,
inference_predicate,
} => Self::Constraint {
name,
inference_predicate: inference_predicate.clone(),
},
}
}
}
impl<T: Model> std::fmt::Debug for ConflictTarget<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Columns {
columns,
inference_predicate,
} => f
.debug_struct("Columns")
.field("columns", columns)
.field("inference_predicate", inference_predicate)
.finish(),
Self::Constraint {
name,
inference_predicate,
} => f
.debug_struct("Constraint")
.field("name", name)
.field("inference_predicate", inference_predicate)
.finish(),
}
}
}
#[must_use = "a ConflictColumns builder is inert until passed to ConflictTarget::columns_of"]
pub struct ConflictColumns<T: Model> {
cols: Vec<&'static str>,
_t: PhantomData<fn() -> T>,
}
impl<T: Model> ConflictColumns<T> {
pub fn new() -> Self {
Self {
cols: Vec::new(),
_t: PhantomData,
}
}
pub fn column<V, C>(mut self, col: C) -> Self
where
C: IntoConflictColumn<T, V>,
{
self.cols.push(col.conflict_column_name());
self
}
}
impl<T: Model> Default for ConflictColumns<T> {
fn default() -> Self {
Self::new()
}
}
pub trait IntoConflictColumn<T: Model, V>: __conflict_sealed::Sealed {
fn conflict_column_name(self) -> &'static str;
}
impl<T: Model, V> __conflict_sealed::Sealed for FieldRef<T, V> {}
impl<T: Model, V> __conflict_sealed::Sealed for DjogiField<T, V> {}
impl<T: Model, V> IntoConflictColumn<T, V> for FieldRef<T, V> {
fn conflict_column_name(self) -> &'static str {
self.column()
}
}
impl<T: Model, V> IntoConflictColumn<T, V> for DjogiField<T, V> {
fn conflict_column_name(self) -> &'static str {
self.into_sql_field().column()
}
}
pub struct ConflictExpr<T: Model, V> {
pub(crate) node: ExprNode,
pub(crate) _marker: PhantomData<fn() -> (T, V)>,
}
impl<T: Model, V> ConflictExpr<T, V> {
pub(crate) fn from_node(node: ExprNode) -> Self {
Self {
node,
_marker: PhantomData,
}
}
}
impl<T: Model, V> Clone for ConflictExpr<T, V> {
fn clone(&self) -> Self {
Self::from_node(self.node.clone())
}
}
impl<T: Model, V> std::fmt::Debug for ConflictExpr<T, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConflictExpr")
.field("node", &self.node)
.finish()
}
}
pub struct ExcludedRef<T: Model, V> {
pub(crate) node: ExprNode,
pub(crate) _marker: PhantomData<fn() -> (T, V)>,
}
impl<T: Model, V> ExcludedRef<T, V> {
pub(crate) fn from_node(node: ExprNode) -> Self {
Self {
node,
_marker: PhantomData,
}
}
#[must_use = "a ConflictExpr is lazy — use it in a DO UPDATE SET assignment"]
pub fn into_conflict_expr(self) -> ConflictExpr<T, V> {
ConflictExpr::from_node(self.node)
}
}
impl<T: Model, V> Clone for ExcludedRef<T, V> {
fn clone(&self) -> Self {
Self::from_node(self.node.clone())
}
}
impl<T: Model, V> std::fmt::Debug for ExcludedRef<T, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExcludedRef")
.field("node", &self.node)
.finish()
}
}
pub struct ConflictCondition<T: Model> {
pub(crate) node: ExprNode,
pub(crate) _marker: PhantomData<fn() -> T>,
}
impl<T: Model> Clone for ConflictCondition<T> {
fn clone(&self) -> Self {
Self {
node: self.node.clone(),
_marker: PhantomData,
}
}
}
impl<T: Model> std::fmt::Debug for ConflictCondition<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConflictCondition")
.field("node", &self.node)
.finish()
}
}
pub trait IntoConflictCondition<T: Model>: __conflict_sealed::Sealed {
fn into_conflict_condition(self) -> ConflictCondition<T>;
}
impl<T: Model> __conflict_sealed::Sealed for ConflictCondition<T> {}
impl<T: Model> IntoConflictCondition<T> for ConflictCondition<T> {
fn into_conflict_condition(self) -> ConflictCondition<T> {
self
}
}
#[derive(Debug)]
pub(crate) struct ConflictUpdateValue {
pub(crate) node: ExprNode,
}
impl Clone for ConflictUpdateValue {
fn clone(&self) -> Self {
Self {
node: self.node.clone(),
}
}
}
pub struct ConflictUpdate<S: Model, T: Model> {
pub(crate) target_column: &'static str,
pub(crate) value: ConflictUpdateValue,
pub(crate) _phantom: PhantomData<fn() -> (S, T)>,
}
impl<S: Model, T: Model> Clone for ConflictUpdate<S, T> {
fn clone(&self) -> Self {
Self {
target_column: self.target_column,
value: self.value.clone(),
_phantom: PhantomData,
}
}
}
impl<S: Model, T: Model> std::fmt::Debug for ConflictUpdate<S, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConflictUpdate")
.field("target_column", &self.target_column)
.field("value", &self.value)
.finish()
}
}
impl<S: Model, T: Model> ConflictUpdate<S, T> {
#[doc(hidden)]
pub(crate) fn target_column(&self) -> &'static str {
self.target_column
}
#[doc(hidden)]
pub(crate) fn value_node(&self) -> &ExprNode {
&self.value.node
}
}
#[non_exhaustive]
pub enum ConflictAction<S: Model, T: Model> {
DoNothing,
#[non_exhaustive]
DoUpdate {
assignments: Vec<ConflictUpdate<S, T>>,
where_clause: Option<Box<ConflictCondition<T>>>,
},
}
impl<S: Model, T: Model> Clone for ConflictAction<S, T> {
fn clone(&self) -> Self {
match self {
Self::DoNothing => Self::DoNothing,
Self::DoUpdate {
assignments,
where_clause,
} => Self::DoUpdate {
assignments: assignments.clone(),
where_clause: where_clause.clone(),
},
}
}
}
impl<S: Model, T: Model> std::fmt::Debug for ConflictAction<S, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DoNothing => f.write_str("DoNothing"),
Self::DoUpdate {
assignments,
where_clause,
} => f
.debug_struct("DoUpdate")
.field("assignments", assignments)
.field("where_clause", where_clause)
.finish(),
}
}
}
pub trait IntoConflictExpr<T: Model, V>: __conflict_sealed::Sealed {
fn into_conflict_expr(self) -> ConflictExpr<T, V>;
}
impl<T: Model, V> __conflict_sealed::Sealed for ExcludedRef<T, V> {}
impl<T: Model, V> __conflict_sealed::Sealed for ConflictExpr<T, V> {}
impl<T: Model, V> IntoConflictExpr<T, V> for FieldRef<T, V> {
fn into_conflict_expr(self) -> ConflictExpr<T, V> {
ConflictExpr::from_node(ExprNode::Field {
column: self.column(),
})
}
}
impl<T: Model, V> IntoConflictExpr<T, V> for DjogiField<T, V> {
fn into_conflict_expr(self) -> ConflictExpr<T, V> {
self.into_sql_field().into_conflict_expr()
}
}
impl<T: Model, V> IntoConflictExpr<T, V> for ExcludedRef<T, V> {
fn into_conflict_expr(self) -> ConflictExpr<T, V> {
ConflictExpr::from_node(self.node)
}
}
impl<T: Model, V> IntoConflictExpr<T, V> for ConflictExpr<T, V> {
fn into_conflict_expr(self) -> ConflictExpr<T, V> {
self
}
}
pub trait IntoConflictUpdates<S: Model, T: Model>: __conflict_sealed::Sealed {
fn into_conflict_updates(self) -> Vec<ConflictUpdate<S, T>>;
}
impl<S: Model, T: Model> __conflict_sealed::Sealed for ConflictUpdate<S, T> {}
impl<S: Model, T: Model> __conflict_sealed::Sealed for Vec<ConflictUpdate<S, T>> {}
impl<S: Model, T: Model> IntoConflictUpdates<S, T> for ConflictUpdate<S, T> {
fn into_conflict_updates(self) -> Vec<ConflictUpdate<S, T>> {
vec![self]
}
}
impl<S: Model, T: Model> IntoConflictUpdates<S, T> for Vec<ConflictUpdate<S, T>> {
fn into_conflict_updates(self) -> Vec<ConflictUpdate<S, T>> {
self
}
}
impl<T: Model> ConflictTarget<T> {
#[must_use]
pub fn columns<I, C, V>(fields: I) -> Self
where
I: IntoIterator<Item = C>,
C: IntoConflictColumn<T, V>,
{
Self::Columns {
columns: fields
.into_iter()
.map(IntoConflictColumn::conflict_column_name)
.collect(),
inference_predicate: None,
}
}
#[must_use]
pub fn columns_of(builder: ConflictColumns<T>) -> Self {
Self::Columns {
columns: builder.cols,
inference_predicate: None,
}
}
#[must_use]
pub fn constraint(name: &'static str) -> Self {
crate::ident::assert_plain_ident(name, "conflict constraint name");
Self::Constraint {
name,
inference_predicate: None,
}
}
#[must_use]
#[allow(clippy::self_named_constructors)]
pub fn none() -> Option<Self> {
None
}
#[must_use]
pub fn where_predicate<F, C>(self, f: F) -> Self
where
F: FnOnce(T::Fields) -> C,
C: IntoConflictCondition<T>,
{
match self {
Self::Columns { columns, .. } => Self::Columns {
columns,
inference_predicate: Some(Box::new(
f(T::Fields::default()).into_conflict_condition(),
)),
},
Self::Constraint { name, .. } => Self::Constraint {
name,
inference_predicate: Some(Box::new(
f(T::Fields::default()).into_conflict_condition(),
)),
},
}
}
}
impl<T: Model> ConflictCondition<T> {
pub fn and(self, other: ConflictCondition<T>) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::And(Box::new(self.node), Box::new(other.node)),
_marker: PhantomData,
}
}
pub fn or(self, other: ConflictCondition<T>) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::Or(Box::new(self.node), Box::new(other.node)),
_marker: PhantomData,
}
}
}
impl<T: Model> std::ops::Not for ConflictCondition<T> {
type Output = ConflictCondition<T>;
fn not(self) -> Self::Output {
ConflictCondition {
node: ExprNode::Not(Box::new(self.node)),
_marker: PhantomData,
}
}
}
fn expr_node_contains_excluded(node: &ExprNode) -> bool {
match node {
ExprNode::Field { .. }
| ExprNode::RawSql(_)
| ExprNode::Literal(_)
| ExprNode::ArrayLength { .. }
| ExprNode::CurrentYear
| ExprNode::OuterRef { .. }
| ExprNode::OuterRefColumn { .. }
| ExprNode::OuterRefAlias { .. }
| ExprNode::TsMatch { .. }
| ExprNode::TsRank { .. }
| ExprNode::TsRankCd { .. }
| ExprNode::IntervalLiteral { .. } => false,
ExprNode::Excluded { .. } => true,
ExprNode::Cmp { lhs, rhs, .. }
| ExprNode::Add(lhs, rhs)
| ExprNode::Sub(lhs, rhs)
| ExprNode::Mul(lhs, rhs)
| ExprNode::Div(lhs, rhs)
| ExprNode::And(lhs, rhs)
| ExprNode::Or(lhs, rhs) => {
expr_node_contains_excluded(lhs) || expr_node_contains_excluded(rhs)
}
ExprNode::Not(inner) | ExprNode::IsNull(inner) | ExprNode::IsNotNull(inner) => {
expr_node_contains_excluded(inner)
}
ExprNode::Coalesce(operands) => operands.iter().any(expr_node_contains_excluded),
ExprNode::GroupingVariadic { args } => args.iter().any(expr_node_contains_excluded),
ExprNode::Case { arms, otherwise } => {
arms.iter().any(|(cond, val)| {
expr_node_contains_excluded(cond) || expr_node_contains_excluded(val)
}) || expr_node_contains_excluded(otherwise)
}
ExprNode::Aggregate {
arg, arg2, filter, ..
} => {
expr_node_contains_excluded(arg)
|| arg2.as_deref().is_some_and(expr_node_contains_excluded)
|| filter.as_deref().is_some_and(expr_node_contains_excluded)
}
ExprNode::Exists(_) | ExprNode::Subquery(_) => false,
ExprNode::InSubquery { lhs, .. } | ExprNode::QuantifiedSubquery { lhs, .. } => {
expr_node_contains_excluded(lhs)
}
#[cfg(feature = "spatial")]
ExprNode::Spatial(_) | ExprNode::RowAggregate { .. } => false,
#[cfg(feature = "trgm")]
ExprNode::TrgmSimilarTo { .. } | ExprNode::TrgmSimilarityScore { .. } => false,
}
}
impl<T: Model, V: Numeric> std::ops::Add for ConflictExpr<T, V> {
type Output = ConflictExpr<T, V>;
fn add(self, rhs: Self) -> Self::Output {
ConflictExpr::from_node(ExprNode::Add(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Sub for ConflictExpr<T, V> {
type Output = ConflictExpr<T, V>;
fn sub(self, rhs: Self) -> Self::Output {
ConflictExpr::from_node(ExprNode::Sub(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Mul for ConflictExpr<T, V> {
type Output = ConflictExpr<T, V>;
fn mul(self, rhs: Self) -> Self::Output {
ConflictExpr::from_node(ExprNode::Mul(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Div for ConflictExpr<T, V> {
type Output = ConflictExpr<T, V>;
fn div(self, rhs: Self) -> Self::Output {
ConflictExpr::from_node(ExprNode::Div(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Add for ExcludedRef<T, V> {
type Output = ExcludedRef<T, V>;
fn add(self, rhs: Self) -> Self::Output {
ExcludedRef::from_node(ExprNode::Add(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Sub for ExcludedRef<T, V> {
type Output = ExcludedRef<T, V>;
fn sub(self, rhs: Self) -> Self::Output {
ExcludedRef::from_node(ExprNode::Sub(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Mul for ExcludedRef<T, V> {
type Output = ExcludedRef<T, V>;
fn mul(self, rhs: Self) -> Self::Output {
ExcludedRef::from_node(ExprNode::Mul(Box::new(self.node), Box::new(rhs.node)))
}
}
impl<T: Model, V: Numeric> std::ops::Div for ExcludedRef<T, V> {
type Output = ExcludedRef<T, V>;
fn div(self, rhs: Self) -> Self::Output {
ExcludedRef::from_node(ExprNode::Div(Box::new(self.node), Box::new(rhs.node)))
}
}
fn conflict_condition<T: Model>(lhs: ExprNode, op: CmpOp, rhs: ExprNode) -> ConflictCondition<T> {
ConflictCondition {
node: ExprNode::Cmp {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
_marker: PhantomData,
}
}
#[must_use = "InsertSelectStmt is inert — call .execute(ctx) to run the INSERT ... SELECT"]
pub struct InsertSelectStmt<S: Model, T: Model> {
pub(crate) source: QuerySet<S>,
pub(crate) columns: Vec<InsertSelectColumn<S, T>>,
pub(crate) on_conflict: Option<OnConflictClause<S, T>>,
pub(crate) _target: PhantomData<fn() -> T>,
}
impl<S: Model, T: Model> Clone for InsertSelectStmt<S, T> {
fn clone(&self) -> Self {
InsertSelectStmt {
source: self.source.clone(),
columns: self.columns.clone(),
on_conflict: self.on_conflict.clone(),
_target: PhantomData,
}
}
}
impl<S: Model, T: Model> std::fmt::Debug for InsertSelectStmt<S, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InsertSelectStmt")
.field("source_table", &S::table_name())
.field("target_table", &T::table_name())
.field("source", &self.source)
.field("columns", &self.columns)
.field("on_conflict", &self.on_conflict)
.finish()
}
}
impl<S: Model, T: Model> InsertSelectStmt<S, T> {
fn validate_execute(&self) -> Result<(), DjogiError> {
if self.columns.is_empty() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: column mapping is empty; an INSERT...SELECT \
with no columns is invalid SQL. The closure passed to \
QuerySet::insert_into must return at least one column mapping \
via FieldRef::copy_from",
T::table_name(),
)));
}
let mut seen: HashSet<&'static str> = HashSet::with_capacity(self.columns.len());
for col in &self.columns {
if !seen.insert(col.target_column) {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: target column '{}' appears more than \
once in the column mapping; Postgres rejects duplicate \
columns in an INSERT column list (SQLSTATE 42701)",
T::table_name(),
col.target_column,
)));
}
}
if !self.source.prefetch_paths.is_empty() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: source queryset has registered prefetch \
paths, which have no meaning for INSERT...SELECT (no rows \
are returned to the caller). Drop the .prefetch(...) calls \
before .insert_into(...)",
T::table_name(),
)));
}
if !self.source.select_related_paths.is_empty() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: source queryset has registered \
select_related paths, which expand the SELECT list with \
aliased joined columns the INSERT...SELECT column-mapping \
closure cannot reference. Drop the .select_related(...) \
calls before .insert_into(...)",
T::table_name(),
)));
}
if self.source.cache_target.is_some() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: source queryset is bound to a Punnu via \
.cache(...). INSERT...SELECT returns the affected row count, \
not rows, so the cache binding has nothing to insert. Drop \
the .cache(...) call before .insert_into(...)",
T::table_name(),
)));
}
if !matches!(self.source.lock, crate::query::lock::LockMode::None) {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: source queryset carries a row-level lock \
(FOR UPDATE / FOR SHARE / NOWAIT / SKIP LOCKED) which is \
not yet supported on INSERT...SELECT in djogi v0.1. Drop \
the .select_for_update() / .nowait() / .skip_locked() / \
.select_for_share() / .for_share_nowait() / \
.for_share_skip_locked() call before .insert_into(...); a \
follow-up issue can lift this restriction with an \
explicit opt-in",
T::table_name(),
)));
}
if !matches!(
self.source.distinct,
crate::query::queryset::DistinctMode::None,
) {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: source queryset carries .distinct() / \
.distinct_on(...) which is not yet supported on \
INSERT...SELECT in djogi v0.1. Drop the .distinct...() call \
before .insert_into(...); a follow-up issue can lift this \
restriction with an explicit opt-in",
T::table_name(),
)));
}
if let Some(clause) = &self.on_conflict {
if let Some(target) = &clause.target {
match target {
ConflictTarget::Columns {
columns,
inference_predicate,
} => {
if columns.is_empty() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict target column list is empty",
T::table_name(),
)));
}
let mut seen: HashSet<&'static str> = HashSet::with_capacity(columns.len());
for col in columns {
if !seen.insert(col) {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict target column '{}' appears more than once",
T::table_name(),
col,
)));
}
}
let known: HashSet<&'static str> =
T::descriptor().fields.iter().map(|f| f.name).collect();
for col in columns {
if !known.contains(col) {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict target column '{}' is not a column of the model",
T::table_name(),
col,
)));
}
}
for col in columns {
if crate::ident::check_plain_ident(col, true).is_err() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict target column '{}' is not a valid Postgres identifier",
T::table_name(),
col,
)));
}
}
if inference_predicate
.as_deref()
.is_some_and(|pred| expr_node_contains_excluded(&pred.node))
{
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict target WHERE predicate cannot reference EXCLUDED; arbiter inference predicates may only reference target-table columns",
T::table_name(),
)));
}
}
ConflictTarget::Constraint {
name,
inference_predicate,
} => {
if crate::ident::check_plain_ident(name, true).is_err() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT conflict constraint name '{}' is not a valid Postgres identifier (must be a non-empty, <=63-byte ASCII identifier that is not a reserved keyword); reject post-construction mutation of ConflictTarget::Constraint::name",
T::table_name(),
name,
)));
}
if inference_predicate.is_some() {
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT ON CONSTRAINT does not accept a WHERE inference predicate; use ConflictTarget::columns(...).where_predicate(...) instead",
T::table_name(),
)));
}
}
}
}
if let ConflictAction::DoUpdate { assignments, .. } = &clause.action
&& assignments.is_empty()
{
return Err(DjogiError::Validation(format!(
"insert_into::<{}>: ON CONFLICT DO UPDATE SET requires at least one assignment",
T::table_name(),
)));
}
}
Ok(())
}
#[must_use = "InsertSelectStmt is inert — call .execute(ctx) to run the INSERT ... SELECT"]
pub fn on_conflict_do_nothing(mut self, target: impl Into<Option<ConflictTarget<T>>>) -> Self {
self.on_conflict = Some(OnConflictClause {
target: target.into(),
action: ConflictAction::DoNothing,
});
self
}
#[must_use = "InsertSelectStmt is inert — call .execute(ctx) to run the INSERT ... SELECT"]
pub fn on_conflict_do_update<F, U>(mut self, target: ConflictTarget<T>, updates: F) -> Self
where
F: FnOnce(T::Fields) -> U,
U: IntoConflictUpdates<S, T>,
{
self.on_conflict = Some(OnConflictClause {
target: Some(target),
action: ConflictAction::DoUpdate {
assignments: updates(T::Fields::default()).into_conflict_updates(),
where_clause: None,
},
});
self
}
#[must_use = "InsertSelectStmt is inert — call .execute(ctx) to run the INSERT ... SELECT"]
pub fn on_conflict_do_update_where<F, U, P, C>(
mut self,
target: ConflictTarget<T>,
updates: F,
predicate: P,
) -> Self
where
F: FnOnce(T::Fields) -> U,
U: IntoConflictUpdates<S, T>,
P: FnOnce(T::Fields) -> C,
C: IntoConflictCondition<T>,
{
self.on_conflict = Some(OnConflictClause {
target: Some(target),
action: ConflictAction::DoUpdate {
assignments: updates(T::Fields::default()).into_conflict_updates(),
where_clause: Some(Box::new(
predicate(T::Fields::default()).into_conflict_condition(),
)),
},
});
self
}
pub fn execute<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<u64, DjogiError>> + Send + 'ctx
where
S: 'ctx,
T: 'ctx,
{
async move {
self.validate_execute()?;
if self.source.is_empty() {
return Ok(0);
}
auto_set_tenant::<T>(ctx).await?;
auto_set_tenant::<S>(ctx).await?;
let acc = build_insert_select_with_conflict::<S, T>(
&self.source,
&self.columns,
self.on_conflict.as_ref(),
)
.map_err(DjogiError::from)?;
let (sql, binds) = acc.into_parts();
let params = as_params(&binds);
let rows_affected = ctx.execute(&sql, ¶ms).await?;
Ok(rows_affected)
}
}
pub fn execute_returning<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Vec<T>, DjogiError>> + Send + 'ctx
where
S: 'ctx,
T: 'ctx + FromPgRow,
{
async move {
self.validate_execute()?;
if self.source.is_empty() {
return Ok(Vec::new());
}
auto_set_tenant::<T>(ctx).await?;
auto_set_tenant::<S>(ctx).await?;
let acc = build_insert_select_returning_with_conflict::<S, T>(
&self.source,
&self.columns,
self.on_conflict.as_ref(),
)
.map_err(DjogiError::from)?;
let (sql, binds) = acc.into_parts();
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
let mut results = Vec::with_capacity(rows.len());
for row in &rows {
results.push(T::from_pg_row(row)?);
}
Ok(results)
}
}
}
impl<S: Model> QuerySet<S> {
#[must_use = "InsertSelectStmt is inert — call .execute(ctx) to run the INSERT ... SELECT"]
pub fn insert_into<T, F, I>(self, f: F) -> InsertSelectStmt<S, T>
where
T: Model,
F: FnOnce(T::Fields, S::Fields) -> I,
I: IntoInsertColumns<S, T>,
{
let columns = f(T::Fields::default(), S::Fields::default()).into_insert_columns();
InsertSelectStmt {
source: self,
columns,
on_conflict: None,
_target: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::{FieldSqlType, ModelDescriptor, PkType, field_descriptor};
use crate::query::field::FieldRef;
#[derive(Default, Clone, Copy)]
struct SourceFields;
#[derive(Default, Clone, Copy)]
struct TargetFields;
impl TargetFields {
fn view_count(self) -> FieldRef<Target, i32> {
FieldRef::new("view_count")
}
fn published(self) -> FieldRef<Target, bool> {
FieldRef::new("published")
}
}
static TARGET_DESCRIPTOR: ModelDescriptor = ModelDescriptor {
type_name: "Target",
table_name: "targets",
pk_type: PkType::HeerIdDesc,
fields: &[
field_descriptor("view_count", FieldSqlType::Integer, false),
field_descriptor("maybe_view_count", FieldSqlType::Integer, true),
field_descriptor("published", FieldSqlType::Boolean, false),
],
partition_by: None,
has_outbox: false,
idempotency_key: None,
tenant_key: None,
cache_ttl: None,
rationale: None,
indexes: &[],
is_through: false,
fts: None,
app: None,
moved_from_app: None,
renamed_from: None,
exclusion_constraints: &[],
tree_edge: None,
proxy_for: None,
default_filter_sql: None,
computed_fields: &[],
table_comment: None,
storage_params: None,
tablespace: None,
};
struct Source;
impl crate::model::__sealed::Sealed for Source {}
#[allow(clippy::manual_async_fn)]
impl Model for Source {
type Pk = i64;
type Fields = SourceFields;
fn table_name() -> &'static str {
"sources"
}
fn pk_value(&self) -> &i64 {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
}
struct Target;
impl crate::model::__sealed::Sealed for Target {}
#[allow(clippy::manual_async_fn)]
impl Model for Target {
type Pk = i64;
type Fields = TargetFields;
fn table_name() -> &'static str {
"targets"
}
fn pk_value(&self) -> &i64 {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
&TARGET_DESCRIPTOR
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
}
#[test]
fn field_ref_copy_from_field_builds_column_mapping() {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
let mapping: InsertSelectColumn<Source, Target> =
target_col.copy_from(source_col.as_insert_source());
assert_eq!(mapping.target_column(), "view_count");
assert!(matches!(
mapping.source(),
ExprNode::Field { column } if *column == "score"
));
}
#[test]
fn field_ref_copy_from_literal_builds_column_mapping() {
let target_col: FieldRef<Target, i32> = FieldRef::new("status_code");
let mapping: InsertSelectColumn<Source, Target> =
target_col.copy_from(InsertSelectSource::<Source, _>::literal(42i32));
assert_eq!(mapping.target_column(), "status_code");
assert!(matches!(mapping.source(), ExprNode::Literal(_)));
}
#[test]
fn into_insert_columns_single_wraps_in_vec() {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
let mapping = target_col.copy_from(source_col.as_insert_source());
let v: Vec<InsertSelectColumn<Source, Target>> = mapping.into_insert_columns();
assert_eq!(v.len(), 1);
assert_eq!(v[0].target_column(), "view_count");
}
#[test]
fn into_insert_columns_vec_passes_through() {
let target_a: FieldRef<Target, i32> = FieldRef::new("view_count");
let target_b: FieldRef<Target, bool> = FieldRef::new("published");
let source_a: FieldRef<Source, i32> = FieldRef::new("score");
let source_b: FieldRef<Source, bool> = FieldRef::new("active");
let vs: Vec<InsertSelectColumn<Source, Target>> = vec![
target_a.copy_from(source_a.as_insert_source()),
target_b.copy_from(source_b.as_insert_source()),
];
let out = vs.into_insert_columns();
assert_eq!(out.len(), 2);
assert_eq!(out[0].target_column(), "view_count");
assert_eq!(out[1].target_column(), "published");
}
#[test]
fn insert_into_builds_stmt_with_columns() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs.insert_into::<Target, _, _>(|_t, _s| {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
vec![target_col.copy_from(source_col.as_insert_source())]
});
assert_eq!(stmt.columns.len(), 1);
assert_eq!(stmt.columns[0].target_column(), "view_count");
}
#[test]
fn insert_select_stmt_clones_preserve_columns() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs.insert_into::<Target, _, _>(|_t, _s| {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
vec![target_col.copy_from(source_col.as_insert_source())]
});
let cloned = stmt.clone();
assert_eq!(cloned.columns.len(), 1);
assert_eq!(cloned.columns[0].target_column(), "view_count");
}
#[test]
fn insert_select_stmt_single_mapping_via_into_insert_columns() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs.insert_into::<Target, _, _>(|_t, _s| {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
target_col.copy_from(source_col.as_insert_source())
});
assert_eq!(stmt.columns.len(), 1);
assert_eq!(stmt.columns[0].target_column(), "view_count");
}
#[test]
fn insert_select_source_arithmetic_composes_same_source_tag() {
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let composed: InsertSelectSource<Source, i32> =
source_col.as_insert_source() + InsertSelectSource::<Source, _>::literal(1i32);
let mapping: InsertSelectColumn<Source, Target> = target_col.copy_from(composed);
assert_eq!(mapping.target_column(), "view_count");
assert!(matches!(mapping.source(), ExprNode::Add(_, _)));
}
#[test]
fn insert_select_stmt_default_has_no_on_conflict() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs.insert_into::<Target, _, _>(|_t, _s| {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
vec![target_col.copy_from(source_col.as_insert_source())]
});
assert!(stmt.on_conflict.is_none());
}
#[test]
fn insert_select_stmt_clone_preserves_on_conflict_none() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs.insert_into::<Target, _, _>(|_t, _s| {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let source_col: FieldRef<Source, i32> = FieldRef::new("score");
vec![target_col.copy_from(source_col.as_insert_source())]
});
let cloned = stmt.clone();
assert!(cloned.on_conflict.is_none());
}
#[test]
fn excluded_builds_pseudo_table_node() {
let col: FieldRef<Target, i32> = FieldRef::new("view_count");
let excl: ExcludedRef<Target, i32> = col.excluded();
assert!(matches!(
excl.node,
ExprNode::Excluded { column } if column == "view_count"
));
}
#[test]
fn excluded_arithmetic_composes() {
let col_a: FieldRef<Target, i32> = FieldRef::new("view_count");
let col_b: FieldRef<Target, i32> = FieldRef::new("view_count");
let composed: ExcludedRef<Target, i32> = col_a.excluded() + col_b.excluded();
assert!(matches!(composed.node, ExprNode::Add(_, _)));
}
#[test]
fn conflict_set_from_excluded_builds_assignment() {
let col: FieldRef<Target, i32> = FieldRef::new("view_count");
let asgn: ConflictUpdate<Source, Target> =
col.conflict_set::<Source>(FieldRef::<Target, i32>::new("view_count").excluded());
assert_eq!(asgn.target_column(), "view_count");
assert!(matches!(
asgn.value_node(),
ExprNode::Excluded { column } if *column == "view_count"
));
}
#[test]
fn conflict_set_expr_builds_arithmetic_assignment() {
let target_col: FieldRef<Target, i32> = FieldRef::new("view_count");
let lhs: FieldRef<Target, i32> = FieldRef::new("view_count");
let rhs: FieldRef<Target, i32> = FieldRef::new("view_count");
let asgn: ConflictUpdate<Source, Target> = target_col.conflict_set_expr::<Source, _>(
lhs.as_conflict_expr() + rhs.excluded().into_conflict_expr(),
);
assert_eq!(asgn.target_column(), "view_count");
assert!(matches!(asgn.value_node(), ExprNode::Add(_, _)));
}
#[test]
fn conflict_is_null_builds_target_postfix_predicate() {
let cond = FieldRef::<Target, Option<i32>>::new("maybe_view_count").conflict_is_null();
assert!(matches!(
cond.node,
ExprNode::IsNull(inner)
if matches!(*inner, ExprNode::Field { column } if column == "maybe_view_count")
));
}
#[test]
fn conflict_is_not_null_builds_excluded_postfix_predicate() {
let cond = FieldRef::<Target, Option<i32>>::new("maybe_view_count")
.excluded()
.conflict_is_not_null();
assert!(matches!(
cond.node,
ExprNode::IsNotNull(inner)
if matches!(*inner, ExprNode::Excluded { column } if column == "maybe_view_count")
));
}
#[test]
fn conflict_coalesce_excluded_builds_same_column_coalesce_expr() {
let expr =
FieldRef::<Target, Option<i32>>::new("maybe_view_count").conflict_coalesce_excluded();
match expr.node {
ExprNode::Coalesce(args) => {
assert_eq!(args.len(), 2);
assert!(
matches!(args[0], ExprNode::Field { column } if column == "maybe_view_count")
);
assert!(
matches!(args[1], ExprNode::Excluded { column } if column == "maybe_view_count")
);
}
other => panic!("expected Coalesce(Field, Excluded), got {other:?}"),
}
}
#[test]
fn conflict_excluded_builds_excluded_assignment() {
let col: FieldRef<Target, i32> = FieldRef::new("view_count");
let asgn: ConflictUpdate<Source, Target> = col.conflict_excluded::<Source>();
assert_eq!(asgn.target_column(), "view_count");
assert!(matches!(
asgn.value_node(),
ExprNode::Excluded { column } if *column == "view_count"
));
}
#[test]
fn conflict_add_builds_add_against_literal() {
let col: FieldRef<Target, i32> = FieldRef::new("view_count");
let asgn: ConflictUpdate<Source, Target> = col.conflict_add::<Source>(5);
match asgn.value_node() {
ExprNode::Add(lhs, rhs) => {
assert!(matches!(**lhs, ExprNode::Field { column } if column == "view_count"));
assert!(matches!(**rhs, ExprNode::Literal(_)));
}
other => panic!("expected Add(Field, Literal), got {other:?}"),
}
}
#[test]
fn conflict_sub_mul_div_build_their_nodes() {
let sub: ConflictUpdate<Source, Target> =
FieldRef::<Target, i32>::new("view_count").conflict_sub::<Source>(1);
assert!(matches!(sub.value_node(), ExprNode::Sub(_, _)));
let mul: ConflictUpdate<Source, Target> =
FieldRef::<Target, i32>::new("view_count").conflict_mul::<Source>(2);
assert!(matches!(mul.value_node(), ExprNode::Mul(_, _)));
let div: ConflictUpdate<Source, Target> =
FieldRef::<Target, i32>::new("view_count").conflict_div::<Source>(2);
assert!(matches!(div.value_node(), ExprNode::Div(_, _)));
}
#[test]
fn on_conflict_do_nothing_columns_attaches_clause() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::columns([FieldRef::<Target, i32>::new(
"view_count",
)]));
let clause = stmt.on_conflict.as_ref().expect("clause attached");
assert!(matches!(clause.action, ConflictAction::DoNothing));
assert!(matches!(
clause.target,
Some(ConflictTarget::Columns { ref columns, .. }) if columns == &["view_count"]
));
}
#[test]
fn on_conflict_do_nothing_bare_has_no_target() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::none());
let clause = stmt.on_conflict.as_ref().unwrap();
assert!(clause.target.is_none());
}
#[test]
fn on_conflict_do_update_attaches_clause() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_update(
ConflictTarget::columns([FieldRef::<Target, i32>::new("view_count")]),
|t| vec![t.view_count().conflict_set(t.view_count().excluded())],
);
let clause = stmt.on_conflict.as_ref().unwrap();
assert!(matches!(clause.action, ConflictAction::DoUpdate { .. }));
}
#[test]
fn validate_rejects_empty_conflict_target_columns() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::Columns {
columns: vec![],
inference_predicate: None,
});
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(err, DjogiError::Validation(ref m) if m.contains("conflict target")));
}
#[test]
fn validate_rejects_duplicate_conflict_target_columns() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::Columns {
columns: vec!["view_count", "view_count"],
inference_predicate: None,
});
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(err, DjogiError::Validation(ref m) if m.contains("more than once")));
}
#[test]
fn validate_rejects_unknown_conflict_target_column() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::Columns {
columns: vec!["ghost_column"],
inference_predicate: None,
});
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(
err,
DjogiError::Validation(ref m) if m.contains("ghost_column") && m.contains("not a column")
));
}
#[test]
fn validate_rejects_empty_do_update_assignments() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_update(
ConflictTarget::columns([FieldRef::<Target, i32>::new("view_count")]),
|_t| Vec::<ConflictUpdate<Source, Target>>::new(),
);
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(err, DjogiError::Validation(ref m) if m.contains("DO UPDATE SET")));
}
#[test]
fn conflict_target_constraint_valid_name_does_not_panic() {
let _ = ConflictTarget::<Target>::constraint("fakes_pkey");
}
#[test]
#[should_panic]
fn conflict_target_constraint_invalid_name_panics() {
let _ = ConflictTarget::<Target>::constraint("fakes; DROP TABLE fakes");
}
#[test]
fn validate_rejects_where_predicate_on_constraint_target() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(
ConflictTarget::<Target>::constraint("oc_targets_slug_key")
.where_predicate(|t| t.published().conflict_is_true()),
);
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(
err,
DjogiError::Validation(ref m)
if m.contains("ON CONSTRAINT") && m.contains("WHERE inference predicate")
));
}
#[test]
fn validate_rejects_excluded_in_conflict_target_where_predicate() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(
ConflictTarget::columns([FieldRef::<Target, i32>::new("view_count")])
.where_predicate(|t| t.view_count().excluded().conflict_gt_value(0)),
);
let err = stmt.validate_execute().unwrap_err();
assert!(matches!(
err,
DjogiError::Validation(ref m) if m.contains("cannot reference EXCLUDED")
));
}
#[test]
fn validate_rejects_excluded_is_null_in_conflict_target_where_predicate() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(
ConflictTarget::columns([FieldRef::<Target, Option<i32>>::new("maybe_view_count")])
.where_predicate(|_t| {
FieldRef::<Target, Option<i32>>::new("maybe_view_count")
.excluded()
.conflict_is_null()
}),
);
let err = stmt.validate_execute().unwrap_err();
assert!(
matches!(err, DjogiError::Validation(ref m) if m.contains("cannot reference EXCLUDED")),
"expected EXCLUDED rejection, got: {err:?}"
);
}
#[test]
fn validate_rejects_excluded_is_not_null_in_conflict_target_where_predicate() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(
ConflictTarget::columns([FieldRef::<Target, Option<i32>>::new("maybe_view_count")])
.where_predicate(|_t| {
FieldRef::<Target, Option<i32>>::new("maybe_view_count")
.excluded()
.conflict_is_not_null()
}),
);
let err = stmt.validate_execute().unwrap_err();
assert!(
matches!(err, DjogiError::Validation(ref m) if m.contains("cannot reference EXCLUDED")),
"expected EXCLUDED rejection, got: {err:?}"
);
}
#[test]
fn validate_rejects_coalesce_excluded_in_conflict_target_where_predicate() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(
ConflictTarget::columns([FieldRef::<Target, Option<i32>>::new("maybe_view_count")])
.where_predicate(|_t| {
FieldRef::<Target, Option<i32>>::new("maybe_view_count").conflict_eq(
FieldRef::<Target, Option<i32>>::new("maybe_view_count")
.conflict_coalesce_excluded(),
)
}),
);
let err = stmt.validate_execute().unwrap_err();
assert!(
matches!(err, DjogiError::Validation(ref m) if m.contains("cannot reference EXCLUDED")),
"expected EXCLUDED rejection, got: {err:?}"
);
}
#[test]
fn validate_rejects_mutated_constraint_name_with_injection() {
let qs: QuerySet<Source> = QuerySet::new();
let mut stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::constraint(
"targets_view_count_key",
));
if let Some(clause) = stmt.on_conflict.as_mut()
&& let Some(ConflictTarget::Constraint { name, .. }) = clause.target.as_mut()
{
*name = "targets'; DROP TABLE targets;--";
}
let err = stmt.validate_execute().unwrap_err();
assert!(
matches!(err, DjogiError::Validation(ref m) if m.contains("conflict constraint name")),
"mutated constraint name must be rejected by validate_execute, got: {err:?}"
);
}
#[test]
fn validate_rejects_mutated_conflict_column_with_bad_ident() {
let qs: QuerySet<Source> = QuerySet::new();
let stmt = qs
.insert_into::<Target, _, _>(|_t, _s| {
let tc: FieldRef<Target, i32> = FieldRef::new("view_count");
let sc: FieldRef<Source, i32> = FieldRef::new("score");
vec![tc.copy_from(sc.as_insert_source())]
})
.on_conflict_do_nothing(ConflictTarget::<Target>::Columns {
columns: vec!["view_count) OR 1=1 --"],
inference_predicate: None,
});
let err = stmt.validate_execute().unwrap_err();
assert!(
matches!(err, DjogiError::Validation(ref m)
if m.contains("not a column") || m.contains("not a valid Postgres identifier")),
"a non-identifier conflict column must be rejected, got: {err:?}"
);
}
}