#![allow(clippy::manual_async_fn)]
use crate::DjogiError;
use crate::context::DjogiContext;
use crate::ident::check_user_supplied_ident;
use crate::model::Model;
use crate::pg::accumulator::{SqlAccumulator, as_params};
use crate::pg::decode::{
FromPgRow, decode_at, decode_narrowed, decode_narrowed_opt, decode_opt_u64_from_decimal,
decode_u64_from_decimal,
};
use crate::query::portable::PortablePredicateError;
use crate::query::portable::SqlEmitContext;
use crate::query::queryset::{DistinctMode, QuerySet};
use crate::query::sql::{emit_q, q_is_vacuously_true};
use std::future::Future;
use std::marker::PhantomData;
const PG_MAX_PARAMS: usize = 65_535;
const MODEL_ALIAS: &str = "__djogi_m";
const SENTINEL_COL: &str = "__djogi_present";
const VALUES_ALIASES: [&str; 6] = [
"__djogi_values_0",
"__djogi_values_1",
"__djogi_values_2",
"__djogi_values_3",
"__djogi_values_4",
"__djogi_values_5",
];
mod sealed {
pub trait SealedValuesScalar {}
pub trait SealedValuesRow {}
pub trait SealedColumns {}
}
pub trait ValuesScalar: sealed::SealedValuesScalar + Clone + Send + Sync + 'static {
const SQL_CAST: &'static str;
fn push_bind_owned(self, acc: &mut SqlAccumulator);
fn push_null(acc: &mut SqlAccumulator);
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError>;
}
macro_rules! impl_scalar_direct {
($T:ty, $CAST:literal) => {
impl sealed::SealedValuesScalar for $T {}
impl ValuesScalar for $T {
const SQL_CAST: &'static str = $CAST;
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
acc.push_bind(self);
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<$T>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_at::<$T>(row, idx, alias)
}
}
impl sealed::SealedValuesScalar for Option<$T> {}
impl ValuesScalar for Option<$T> {
const SQL_CAST: &'static str = $CAST;
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
acc.push_bind(self);
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<$T>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_at::<Option<$T>>(row, idx, alias)
}
}
};
}
impl_scalar_direct!(String, "TEXT");
impl_scalar_direct!(i16, "SMALLINT");
impl_scalar_direct!(i32, "INTEGER");
impl_scalar_direct!(i64, "BIGINT");
impl_scalar_direct!(f32, "REAL");
impl_scalar_direct!(f64, "DOUBLE PRECISION");
impl_scalar_direct!(bool, "BOOLEAN");
impl_scalar_direct!(rust_decimal::Decimal, "NUMERIC");
impl_scalar_direct!(uuid::Uuid, "UUID");
impl_scalar_direct!(time::OffsetDateTime, "TIMESTAMPTZ");
impl_scalar_direct!(time::PrimitiveDateTime, "TIMESTAMP");
impl_scalar_direct!(time::Date, "DATE");
impl_scalar_direct!(time::Time, "TIME");
impl_scalar_direct!(Vec<u8>, "BYTEA");
impl_scalar_direct!(crate::HeerId, "BIGINT");
impl_scalar_direct!(crate::HeerIdDesc, "BIGINT");
impl_scalar_direct!(crate::RanjId, "UUID");
impl_scalar_direct!(crate::RanjIdDesc, "UUID");
impl_scalar_direct!(crate::Interval, "INTERVAL");
macro_rules! impl_scalar_widened {
($N:ty, $W:ty, $CAST:literal, $widen:expr) => {
impl sealed::SealedValuesScalar for $N {}
impl ValuesScalar for $N {
const SQL_CAST: &'static str = $CAST;
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
let wide: $W = $widen(self);
acc.push_bind(wide);
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<$W>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_narrowed::<$W, $N>(row, idx, alias)
}
}
impl sealed::SealedValuesScalar for Option<$N> {}
impl ValuesScalar for Option<$N> {
const SQL_CAST: &'static str = $CAST;
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
acc.push_bind(self.map(|v| {
let w: $W = $widen(v);
w
}));
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<$W>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_narrowed_opt::<$W, $N>(row, idx, alias)
}
}
};
}
impl_scalar_widened!(i8, i16, "SMALLINT", i16::from);
impl_scalar_widened!(u8, i16, "SMALLINT", i16::from);
impl_scalar_widened!(u16, i32, "INTEGER", i32::from);
impl_scalar_widened!(u32, i64, "BIGINT", i64::from);
impl sealed::SealedValuesScalar for u64 {}
impl ValuesScalar for u64 {
const SQL_CAST: &'static str = "NUMERIC";
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
acc.push_bind(rust_decimal::Decimal::from(self));
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<rust_decimal::Decimal>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_u64_from_decimal(row, idx, alias)
}
}
impl sealed::SealedValuesScalar for Option<u64> {}
impl ValuesScalar for Option<u64> {
const SQL_CAST: &'static str = "NUMERIC";
fn push_bind_owned(self, acc: &mut SqlAccumulator) {
acc.push_bind(self.map(rust_decimal::Decimal::from));
}
fn push_null(acc: &mut SqlAccumulator) {
acc.push_bind(None::<rust_decimal::Decimal>);
}
fn decode_values_col(
row: &tokio_postgres::Row,
idx: usize,
alias: &'static str,
) -> Result<Self, DjogiError> {
decode_opt_u64_from_decimal(row, idx, alias)
}
}
pub trait IntoValuesColumns: sealed::SealedColumns {
fn into_col_vec(self) -> Vec<&'static str>;
}
impl sealed::SealedColumns for (&'static str,) {}
impl IntoValuesColumns for (&'static str,) {
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0]
}
}
impl sealed::SealedColumns for (&'static str, &'static str) {}
impl IntoValuesColumns for (&'static str, &'static str) {
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0, self.1]
}
}
impl sealed::SealedColumns for (&'static str, &'static str, &'static str) {}
impl IntoValuesColumns for (&'static str, &'static str, &'static str) {
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0, self.1, self.2]
}
}
impl sealed::SealedColumns for (&'static str, &'static str, &'static str, &'static str) {}
impl IntoValuesColumns for (&'static str, &'static str, &'static str, &'static str) {
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0, self.1, self.2, self.3]
}
}
impl sealed::SealedColumns
for (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
)
{
}
impl IntoValuesColumns
for (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
)
{
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0, self.1, self.2, self.3, self.4]
}
}
impl sealed::SealedColumns
for (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
)
{
}
impl IntoValuesColumns
for (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
)
{
fn into_col_vec(self) -> Vec<&'static str> {
vec![self.0, self.1, self.2, self.3, self.4, self.5]
}
}
pub trait ValuesRow: sealed::SealedValuesRow + Clone + Send + Sync + 'static {
type Columns: IntoValuesColumns;
const ARITY: usize;
fn sql_casts() -> Vec<&'static str>;
fn push_row_binds_first(self, acc: &mut SqlAccumulator);
fn push_row_binds_rest(self, acc: &mut SqlAccumulator);
fn decode_from(row: &tokio_postgres::Row, start_idx: usize) -> Result<Self, DjogiError>;
}
#[inline]
fn push_bind_with_cast<V: ValuesScalar>(v: V, acc: &mut SqlAccumulator) {
v.push_bind_owned(acc);
acc.push_sql("::");
acc.push_sql(V::SQL_CAST);
}
macro_rules! impl_values_row {
( $arity:expr ; $col_tuple:ty ; $( $idx:tt $T:ident ),+ ) => {
impl< $($T: ValuesScalar),+ > sealed::SealedValuesRow for ( $($T,)+ ) {}
impl< $($T: ValuesScalar),+ > ValuesRow for ( $($T,)+ ) {
type Columns = $col_tuple;
const ARITY: usize = $arity;
fn sql_casts() -> Vec<&'static str> {
vec![ $($T::SQL_CAST),+ ]
}
fn push_row_binds_first(self, acc: &mut SqlAccumulator) {
acc.push_sql("(");
let mut _first = true;
$(
if !_first { acc.push_sql(", "); }
push_bind_with_cast::<$T>(self.$idx, acc);
_first = false;
)+
acc.push_sql(")");
}
fn push_row_binds_rest(self, acc: &mut SqlAccumulator) {
acc.push_sql("(");
let mut _first = true;
$(
if !_first { acc.push_sql(", "); }
self.$idx.push_bind_owned(acc);
_first = false;
)+
acc.push_sql(")");
}
fn decode_from(
row: &tokio_postgres::Row,
start_idx: usize,
) -> Result<Self, DjogiError> {
Ok(( $( $T::decode_values_col(row, start_idx + $idx, VALUES_ALIASES[$idx])?, )+ ))
}
}
};
}
impl_values_row!(1; (&'static str,); 0 A);
impl_values_row!(2; (&'static str, &'static str); 0 A, 1 B);
impl_values_row!(3; (&'static str, &'static str, &'static str); 0 A, 1 B, 2 C);
impl_values_row!(4; (&'static str, &'static str, &'static str, &'static str); 0 A, 1 B, 2 C, 3 D);
impl_values_row!(5; (&'static str, &'static str, &'static str, &'static str, &'static str); 0 A, 1 B, 2 C, 3 D, 4 E);
impl_values_row!(6; (&'static str, &'static str, &'static str, &'static str, &'static str, &'static str); 0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
pub struct ValuesFieldRef<V> {
pub(crate) col_idx: usize,
_v: PhantomData<fn() -> V>,
}
impl<V> ValuesFieldRef<V> {
pub(crate) fn new(col_idx: usize) -> Self {
Self {
col_idx,
_v: PhantomData,
}
}
}
impl<V> Copy for ValuesFieldRef<V> {}
impl<V> Clone for ValuesFieldRef<V> {
fn clone(&self) -> Self {
*self
}
}
impl<V> std::fmt::Debug for ValuesFieldRef<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ValuesFieldRef(col_idx={})", self.col_idx)
}
}
pub struct ValuesFields<Row>(PhantomData<fn() -> Row>);
impl<Row> Default for ValuesFields<Row> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<Row> Copy for ValuesFields<Row> {}
impl<Row> Clone for ValuesFields<Row> {
fn clone(&self) -> Self {
*self
}
}
impl<A: ValuesScalar> ValuesFields<(A,)> {
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
}
impl<A: ValuesScalar, B: ValuesScalar> ValuesFields<(A, B)> {
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
pub fn col1(self) -> ValuesFieldRef<B> {
ValuesFieldRef::new(1)
}
}
impl<A: ValuesScalar, B: ValuesScalar, C: ValuesScalar> ValuesFields<(A, B, C)> {
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
pub fn col1(self) -> ValuesFieldRef<B> {
ValuesFieldRef::new(1)
}
pub fn col2(self) -> ValuesFieldRef<C> {
ValuesFieldRef::new(2)
}
}
impl<A: ValuesScalar, B: ValuesScalar, C: ValuesScalar, D: ValuesScalar>
ValuesFields<(A, B, C, D)>
{
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
pub fn col1(self) -> ValuesFieldRef<B> {
ValuesFieldRef::new(1)
}
pub fn col2(self) -> ValuesFieldRef<C> {
ValuesFieldRef::new(2)
}
pub fn col3(self) -> ValuesFieldRef<D> {
ValuesFieldRef::new(3)
}
}
impl<A: ValuesScalar, B: ValuesScalar, C: ValuesScalar, D: ValuesScalar, E: ValuesScalar>
ValuesFields<(A, B, C, D, E)>
{
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
pub fn col1(self) -> ValuesFieldRef<B> {
ValuesFieldRef::new(1)
}
pub fn col2(self) -> ValuesFieldRef<C> {
ValuesFieldRef::new(2)
}
pub fn col3(self) -> ValuesFieldRef<D> {
ValuesFieldRef::new(3)
}
pub fn col4(self) -> ValuesFieldRef<E> {
ValuesFieldRef::new(4)
}
}
impl<
A: ValuesScalar,
B: ValuesScalar,
C: ValuesScalar,
D: ValuesScalar,
E: ValuesScalar,
F: ValuesScalar,
> ValuesFields<(A, B, C, D, E, F)>
{
pub fn col0(self) -> ValuesFieldRef<A> {
ValuesFieldRef::new(0)
}
pub fn col1(self) -> ValuesFieldRef<B> {
ValuesFieldRef::new(1)
}
pub fn col2(self) -> ValuesFieldRef<C> {
ValuesFieldRef::new(2)
}
pub fn col3(self) -> ValuesFieldRef<D> {
ValuesFieldRef::new(3)
}
pub fn col4(self) -> ValuesFieldRef<E> {
ValuesFieldRef::new(4)
}
pub fn col5(self) -> ValuesFieldRef<F> {
ValuesFieldRef::new(5)
}
}
pub struct ValuesOn<T: Model>(ValuesOnKind<T>);
enum ValuesOnKind<T: Model> {
Eq {
model_col: &'static str,
values_col_idx: usize,
#[doc(hidden)]
_phantom: PhantomData<fn() -> T>,
},
And(Box<ValuesOnKind<T>>, Box<ValuesOnKind<T>>),
}
impl<T: Model> ValuesOn<T> {
fn eq(model_col: &'static str, values_col_idx: usize) -> Self {
Self(ValuesOnKind::Eq {
model_col,
values_col_idx,
_phantom: PhantomData,
})
}
}
impl<T: Model> std::ops::BitAnd for ValuesOn<T> {
type Output = ValuesOn<T>;
fn bitand(self, rhs: ValuesOn<T>) -> ValuesOn<T> {
ValuesOn(ValuesOnKind::And(Box::new(self.0), Box::new(rhs.0)))
}
}
impl<T: Model> std::fmt::Debug for ValuesOn<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt_values_on_kind(&self.0, f)
}
}
fn fmt_values_on_kind<T: Model>(
on: &ValuesOnKind<T>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match on {
ValuesOnKind::Eq {
model_col,
values_col_idx,
..
} => write!(f, "ValuesOn::Eq({model_col} = col{values_col_idx})"),
ValuesOnKind::And(l, r) => {
write!(f, "ValuesOn::And(")?;
fmt_values_on_kind(l, f)?;
write!(f, ", ")?;
fmt_values_on_kind(r, f)?;
write!(f, ")")
}
}
}
impl<M: Model, V: ValuesScalar> crate::query::field::FieldRef<M, V> {
pub fn eq_values(self, rhs: ValuesFieldRef<V>) -> ValuesOn<M> {
ValuesOn::eq(self.column(), rhs.col_idx)
}
}
impl<M: Model, V: ValuesScalar> crate::query::field::DjogiField<M, V> {
pub fn eq_values(self, rhs: ValuesFieldRef<V>) -> ValuesOn<M> {
self.__sql_field().eq_values(rhs)
}
}
pub struct InlineValues<Row: ValuesRow> {
pub(crate) rows: Vec<Row>,
pub(crate) alias: String,
pub(crate) columns: Vec<&'static str>,
}
impl<Row: ValuesRow> Clone for InlineValues<Row> {
fn clone(&self) -> Self {
InlineValues {
rows: self.rows.clone(),
alias: self.alias.clone(),
columns: self.columns.clone(),
}
}
}
impl<Row: ValuesRow> std::fmt::Debug for InlineValues<Row> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InlineValues")
.field("alias", &self.alias)
.field("columns", &self.columns)
.field("row_count", &self.rows.len())
.finish()
}
}
impl<Row: ValuesRow> InlineValues<Row> {
pub fn new(rows: Vec<Row>, alias: &str, columns: Row::Columns) -> Result<Self, DjogiError> {
check_user_supplied_ident(alias, true).map_err(|e| {
DjogiError::Validation(format!(
"InlineValues alias {alias:?} is invalid: {e:?}. \
Supply a plain SQL identifier that does not start with `__djogi_`."
))
})?;
let col_vec = columns.into_col_vec();
debug_assert_eq!(
col_vec.len(),
Row::ARITY,
"InlineValues column count mismatch (framework bug)"
);
for col in &col_vec {
check_user_supplied_ident(col, true).map_err(|e| {
DjogiError::Validation(format!(
"InlineValues column name {col:?} is invalid: {e:?}. \
Supply plain SQL identifiers that do not start with `__djogi_`."
))
})?;
}
{
let mut seen = std::collections::HashSet::with_capacity(col_vec.len());
for col in &col_vec {
let folded = col.to_ascii_lowercase();
if !seen.insert(folded) {
return Err(DjogiError::Validation(format!(
"InlineValues column {col:?} appears more than once \
after Postgres identifier case folding; duplicate \
column names are not allowed."
)));
}
}
}
let param_count = rows.len().checked_mul(Row::ARITY).ok_or_else(|| {
DjogiError::Validation(
"InlineValues parameter count overflowed; \
chunk the list or use a staging table."
.into(),
)
})?;
if param_count > PG_MAX_PARAMS {
return Err(DjogiError::Validation(format!(
"InlineValues would require {param_count} bind parameters \
({} rows × {} columns), exceeding Postgres' limit of {PG_MAX_PARAMS}. \
Chunk the list into smaller batches or load it into a \
temporary/staging table before joining.",
rows.len(),
Row::ARITY,
)));
}
Ok(InlineValues {
rows,
alias: alias.to_owned(),
columns: col_vec,
})
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn len(&self) -> usize {
self.rows.len()
}
}
pub struct ValuesJoinedQuerySet<T: Model, Row: ValuesRow> {
pub(crate) left: QuerySet<T>,
pub(crate) values: InlineValues<Row>,
pub(crate) on: ValuesOn<T>,
}
impl<T: Model, Row: ValuesRow> std::fmt::Debug for ValuesJoinedQuerySet<T, Row> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValuesJoinedQuerySet")
.field("values", &self.values)
.field("on", &self.on)
.finish()
}
}
pub struct LeftValuesJoinedQuerySet<T: Model, Row: ValuesRow> {
pub(crate) left: QuerySet<T>,
pub(crate) values: InlineValues<Row>,
pub(crate) on: ValuesOn<T>,
}
impl<T: Model, Row: ValuesRow> std::fmt::Debug for LeftValuesJoinedQuerySet<T, Row> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LeftValuesJoinedQuerySet")
.field("values", &self.values)
.field("on", &self.on)
.finish()
}
}
pub struct CrossValuesJoinedQuerySet<T: Model, Row: ValuesRow> {
pub(crate) left: QuerySet<T>,
pub(crate) values: InlineValues<Row>,
}
impl<T: Model, Row: ValuesRow> std::fmt::Debug for CrossValuesJoinedQuerySet<T, Row> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CrossValuesJoinedQuerySet")
.field("values", &self.values)
.finish()
}
}
impl<T: Model> QuerySet<T> {
pub fn join_values<Row, F>(
self,
values: InlineValues<Row>,
on_fn: F,
) -> ValuesJoinedQuerySet<T, Row>
where
Row: ValuesRow,
F: FnOnce(T::Fields, ValuesFields<Row>) -> ValuesOn<T>,
{
let on = on_fn(Default::default(), Default::default());
ValuesJoinedQuerySet {
left: self,
values,
on,
}
}
pub fn left_join_values<Row, F>(
self,
values: InlineValues<Row>,
on_fn: F,
) -> LeftValuesJoinedQuerySet<T, Row>
where
Row: ValuesRow,
F: FnOnce(T::Fields, ValuesFields<Row>) -> ValuesOn<T>,
{
let on = on_fn(Default::default(), Default::default());
LeftValuesJoinedQuerySet {
left: self,
values,
on,
}
}
pub fn cross_join_values<Row>(
self,
values: InlineValues<Row>,
) -> CrossValuesJoinedQuerySet<T, Row>
where
Row: ValuesRow,
{
CrossValuesJoinedQuerySet { left: self, values }
}
}
fn validate_left_qs<T: Model>(qs: &QuerySet<T>, site: &str) -> Result<(), DjogiError> {
if !qs.prefetch_paths.is_empty() {
return Err(DjogiError::Validation(format!(
"{site}: left queryset has prefetch paths, which change the row shape. \
Drop .prefetch(…) calls before \
.join_values(…) / .left_join_values(…) / .cross_join_values(…)."
)));
}
if !qs.select_related_paths.is_empty() {
return Err(DjogiError::Validation(format!(
"{site}: left queryset has select_related paths, which expand the \
SELECT list incompatibly. Drop .select_related(…) calls before \
.join_values(…) / .left_join_values(…) / .cross_join_values(…)."
)));
}
if qs.cache_target.is_some() {
return Err(DjogiError::Validation(format!(
"{site}: left queryset is bound to a Punnu via .cache(…). \
VALUES join terminals return pairs, not bare model rows. \
Drop the .cache(…) call before \
.join_values(…) / .left_join_values(…) / .cross_join_values(…)."
)));
}
if !matches!(qs.lock, crate::query::lock::LockMode::None) {
return Err(DjogiError::Validation(format!(
"{site}: left queryset carries a row-level lock, which is not \
supported on VALUES joins. Drop the row-lock call before \
.join_values(…) / .left_join_values(…) / .cross_join_values(…)."
)));
}
if !matches!(qs.distinct, DistinctMode::None) {
return Err(DjogiError::Validation(format!(
"{site}: left queryset carries a non-default DISTINCT mode, which \
is not supported on VALUES joins. Drop .distinct…() calls before \
.join_values(…) / .left_join_values(…) / .cross_join_values(…)."
)));
}
Ok(())
}
fn validate_total_bind_count(
acc: SqlAccumulator,
site: &str,
) -> Result<SqlAccumulator, DjogiError> {
let bind_count = usize::try_from(acc.bind_count()).expect("u32 bind count fits in usize");
if bind_count > PG_MAX_PARAMS {
return Err(DjogiError::Validation(format!(
"{site}: query would require {bind_count} bind parameters after \
composing VALUES rows with filters/pagination, exceeding \
Postgres' limit of {PG_MAX_PARAMS}. Chunk the VALUES input or \
reduce extra bind-producing filters/limit/offset."
)));
}
Ok(acc)
}
fn validate_values_on<T: Model>(on: &ValuesOn<T>, site: &str) -> Result<(), DjogiError> {
validate_values_on_kind(&on.0, site)
}
fn validate_values_on_kind<T: Model>(on: &ValuesOnKind<T>, site: &str) -> Result<(), DjogiError> {
match on {
ValuesOnKind::Eq { model_col, .. } => {
if model_col.contains('.') {
return Err(DjogiError::Validation(format!(
"{site}: VALUES join predicates only support root-model columns; \
relation-path field `{model_col}` is not supported in `.eq_values(...)`."
)));
}
Ok(())
}
ValuesOnKind::And(l, r) => {
validate_values_on_kind(l, site)?;
validate_values_on_kind(r, site)
}
}
}
pub(crate) fn build_values_join_select<T, Row>(
vqs: &ValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("");
emit_select_projection::<T, Row>(&vqs.values, &mut acc, false);
acc.push_sql(" FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_inner_join_values(&vqs.values, &vqs.on, &mut acc);
push_qualified_tail(&mut acc, &vqs.left)?;
Ok(acc)
}
pub(crate) fn build_left_values_join_select<T, Row>(
vqs: &LeftValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("");
emit_select_projection::<T, Row>(&vqs.values, &mut acc, true);
acc.push_sql(" FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_left_join_values(&vqs.values, &vqs.on, &mut acc);
push_qualified_tail(&mut acc, &vqs.left)?;
Ok(acc)
}
pub(crate) fn build_left_values_join_empty_select<T, Row>(
vqs: &LeftValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let values = &vqs.values;
let on = &vqs.on;
let mut acc = SqlAccumulator::new("");
emit_select_projection::<T, Row>(values, &mut acc, true);
acc.push_sql(" FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
acc.push_sql(" LEFT JOIN (SELECT ");
let casts = Row::sql_casts();
for (i, user_col) in values.columns.iter().enumerate() {
if i > 0 {
acc.push_sql(", ");
}
acc.push_sql("NULL::");
acc.push_sql(casts[i]);
acc.push_sql(" AS ");
acc.push_sql(user_col);
}
if !values.columns.is_empty() {
acc.push_sql(", ");
}
acc.push_sql("NULL::BOOLEAN AS ");
acc.push_sql(SENTINEL_COL);
acc.push_sql(" WHERE 1=0) AS ");
acc.push_sql(&values.alias);
acc.push_sql(" ON ");
push_on_predicate(on, values, &mut acc);
push_qualified_tail(&mut acc, &vqs.left)?;
Ok(acc)
}
pub(crate) fn build_values_join_count<T, Row>(
vqs: &ValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT COUNT(*) FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_inner_join_values(&vqs.values, &vqs.on, &mut acc);
push_qualified_where(&mut acc, &vqs.left)?;
Ok(acc)
}
pub(crate) fn build_left_values_join_count<T, Row>(
vqs: &LeftValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT COUNT(*) FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
if !vqs.values.is_empty() {
push_left_join_values(&vqs.values, &vqs.on, &mut acc);
}
push_qualified_where(&mut acc, &vqs.left)?;
Ok(acc)
}
pub(crate) fn build_values_join_exists<T, Row>(
vqs: &ValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT EXISTS (SELECT 1 FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_inner_join_values(&vqs.values, &vqs.on, &mut acc);
push_qualified_where(&mut acc, &vqs.left)?;
acc.push_sql(")");
Ok(acc)
}
pub(crate) fn build_left_values_join_exists<T, Row>(
vqs: &LeftValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT EXISTS(SELECT 1 FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_qualified_where(&mut acc, &vqs.left)?;
acc.push_sql(" LIMIT 1)");
Ok(acc)
}
pub(crate) fn build_cross_values_join_select<T, Row>(
cqs: &CrossValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("");
emit_select_projection::<T, Row>(&cqs.values, &mut acc, false);
acc.push_sql(" FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_cross_join_values(&cqs.values, &mut acc);
push_qualified_tail(&mut acc, &cqs.left)?;
Ok(acc)
}
pub(crate) fn build_cross_values_join_count<T, Row>(
cqs: &CrossValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT COUNT(*) FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_cross_join_values(&cqs.values, &mut acc);
push_qualified_where(&mut acc, &cqs.left)?;
Ok(acc)
}
pub(crate) fn build_cross_values_join_exists<T, Row>(
cqs: &CrossValuesJoinedQuerySet<T, Row>,
) -> Result<SqlAccumulator, PortablePredicateError>
where
T: Model + FromPgRow,
Row: ValuesRow,
{
let mut acc = SqlAccumulator::new("SELECT EXISTS (SELECT 1 FROM ");
acc.push_sql(T::table_name());
acc.push_sql(" AS ");
acc.push_sql(MODEL_ALIAS);
push_cross_join_values(&cqs.values, &mut acc);
push_qualified_where(&mut acc, &cqs.left)?;
acc.push_sql(")");
Ok(acc)
}
fn emit_select_projection<T: Model + FromPgRow, Row: ValuesRow>(
values: &InlineValues<Row>,
acc: &mut SqlAccumulator,
with_sentinel: bool,
) {
acc.push_sql("SELECT ");
for (i, col) in <T as FromPgRow>::COLUMNS.iter().enumerate() {
if i > 0 {
acc.push_sql(", ");
}
acc.push_sql(MODEL_ALIAS);
acc.push_sql(".");
acc.push_sql(col);
acc.push_sql(" AS ");
acc.push_sql(col);
}
for (i, user_col) in values.columns.iter().enumerate() {
acc.push_sql(", ");
acc.push_sql(&values.alias);
acc.push_sql(".");
acc.push_sql(user_col);
acc.push_sql(" AS ");
acc.push_sql(VALUES_ALIASES[i]);
}
if with_sentinel {
acc.push_sql(", ");
acc.push_sql(&values.alias);
acc.push_sql(".");
acc.push_sql(SENTINEL_COL);
acc.push_sql(" AS __djogi_values_present");
}
}
fn push_inner_join_values<Row: ValuesRow, T: Model>(
values: &InlineValues<Row>,
on: &ValuesOn<T>,
acc: &mut SqlAccumulator,
) {
acc.push_sql(" INNER JOIN (VALUES ");
push_values_rows(values, acc, false);
acc.push_sql(") AS ");
acc.push_sql(&values.alias);
push_col_list(values, acc, false);
acc.push_sql(" ON ");
push_on_predicate(on, values, acc);
}
fn push_left_join_values<Row: ValuesRow, T: Model>(
values: &InlineValues<Row>,
on: &ValuesOn<T>,
acc: &mut SqlAccumulator,
) {
acc.push_sql(" LEFT JOIN (VALUES ");
push_values_rows(values, acc, true);
acc.push_sql(") AS ");
acc.push_sql(&values.alias);
push_col_list(values, acc, true);
acc.push_sql(" ON ");
push_on_predicate(on, values, acc);
}
fn push_cross_join_values<Row: ValuesRow>(values: &InlineValues<Row>, acc: &mut SqlAccumulator) {
acc.push_sql(" CROSS JOIN (VALUES ");
push_values_rows(values, acc, false);
acc.push_sql(") AS ");
acc.push_sql(&values.alias);
push_col_list(values, acc, false);
}
fn push_values_rows<Row: ValuesRow>(
values: &InlineValues<Row>,
acc: &mut SqlAccumulator,
with_sentinel: bool,
) {
let mut rows = values.rows.iter().cloned();
if let Some(first) = rows.next() {
first.push_row_binds_first(acc);
if with_sentinel {
let popped = acc.pop_sql_suffix(")");
debug_assert!(popped, "push_row_binds_first must end with ')'");
acc.push_sql(", TRUE)");
}
}
for row in rows {
acc.push_sql(", ");
row.push_row_binds_rest(acc);
if with_sentinel {
let popped = acc.pop_sql_suffix(")");
debug_assert!(popped, "push_row_binds_rest must end with ')'");
acc.push_sql(", TRUE)");
}
}
}
fn push_col_list<Row: ValuesRow>(
values: &InlineValues<Row>,
acc: &mut SqlAccumulator,
with_sentinel: bool,
) {
acc.push_sql("(");
for (i, col) in values.columns.iter().enumerate() {
if i > 0 {
acc.push_sql(", ");
}
acc.push_sql(col);
}
if with_sentinel {
if !values.columns.is_empty() {
acc.push_sql(", ");
}
acc.push_sql(SENTINEL_COL);
}
acc.push_sql(")");
}
fn push_on_predicate<T: Model, Row: ValuesRow>(
on: &ValuesOn<T>,
values: &InlineValues<Row>,
acc: &mut SqlAccumulator,
) {
push_on_predicate_kind(&on.0, values, acc);
}
fn push_on_predicate_kind<T: Model, Row: ValuesRow>(
on: &ValuesOnKind<T>,
values: &InlineValues<Row>,
acc: &mut SqlAccumulator,
) {
match on {
ValuesOnKind::Eq {
model_col,
values_col_idx,
..
} => {
acc.push_sql(MODEL_ALIAS);
acc.push_sql(".");
acc.push_sql(model_col);
acc.push_sql(" = ");
acc.push_sql(&values.alias);
acc.push_sql(".");
acc.push_sql(values.columns[*values_col_idx]);
}
ValuesOnKind::And(l, r) => {
acc.push_sql("(");
push_on_predicate_kind(l, values, acc);
acc.push_sql(" AND ");
push_on_predicate_kind(r, values, acc);
acc.push_sql(")");
}
}
}
fn push_qualified_tail<T: Model>(
acc: &mut SqlAccumulator,
qs: &QuerySet<T>,
) -> Result<(), PortablePredicateError> {
push_qualified_where(acc, qs)?;
if !qs.ordering.is_empty() {
acc.push_sql(" ORDER BY ");
for (i, o) in qs.ordering.iter().enumerate() {
if i > 0 {
acc.push_sql(", ");
}
o.emit(acc, Some(MODEL_ALIAS));
}
}
if let Some(n) = qs.limit {
acc.push_sql(" LIMIT ");
acc.push_bind(n);
}
if let Some(n) = qs.offset {
acc.push_sql(" OFFSET ");
acc.push_bind(n);
}
Ok(())
}
fn push_qualified_where<T: Model>(
acc: &mut SqlAccumulator,
qs: &QuerySet<T>,
) -> Result<(), PortablePredicateError> {
if q_is_vacuously_true(&qs.condition) {
return Ok(());
}
acc.push_sql(" WHERE ");
emit_q::<T>(acc, &qs.condition, SqlEmitContext::joined(MODEL_ALIAS))
}
fn decode_inner_pair<T: Model + FromPgRow, Row: ValuesRow>(
pg_row: &tokio_postgres::Row,
) -> Result<(T, Row), DjogiError> {
let model = T::from_pg_row(pg_row)?;
let row = Row::decode_from(pg_row, <T as FromPgRow>::COLUMNS.len())?;
Ok((model, row))
}
fn decode_left_pair<T: Model + FromPgRow, Row: ValuesRow>(
pg_row: &tokio_postgres::Row,
model_col_count: usize,
) -> Result<(T, Option<Row>), DjogiError> {
let model = T::from_pg_row(pg_row)?;
let sentinel_idx = model_col_count + Row::ARITY;
let present: Option<bool> = pg_row
.try_get::<_, Option<bool>>(sentinel_idx)
.map_err(|e| {
DjogiError::Decode(format!(
"column `__djogi_values_present` at position {sentinel_idx}: {e}"
))
})?;
let values_row = if present.unwrap_or(false) {
Some(Row::decode_from(pg_row, model_col_count)?)
} else {
None
};
Ok((model, values_row))
}
impl<T, Row> ValuesJoinedQuerySet<T, Row>
where
T: Model + FromPgRow + Send + Unpin,
Row: ValuesRow + Send + Unpin,
{
pub fn fetch_all<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Vec<(T, Row)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "join_values::fetch_all")?;
validate_values_on(&self.on, "join_values::fetch_all")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(vec![]);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_values_join_select(&self).map_err(DjogiError::from)?,
"join_values::fetch_all",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_all(&sql, ¶ms)
.await?
.iter()
.map(|r| decode_inner_pair::<T, Row>(r))
.collect()
}
}
pub fn first<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Option<(T, Row)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "join_values::first")?;
validate_values_on(&self.on, "join_values::first")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(None);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let mut limited = self;
limited.left.limit = Some(1);
let (sql, binds) = validate_total_bind_count(
build_values_join_select(&limited).map_err(DjogiError::from)?,
"join_values::first",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_opt(&sql, ¶ms)
.await?
.as_ref()
.map(|r| decode_inner_pair::<T, Row>(r))
.transpose()
}
}
pub fn fetch_one<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<(T, Row), DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "join_values::fetch_one")?;
validate_values_on(&self.on, "join_values::fetch_one")?;
if self.left.is_empty() || self.values.is_empty() {
return Err(DjogiError::not_found(T::table_name()));
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let mut probe = self;
probe.left.limit = Some(2);
let (sql, binds) = validate_total_bind_count(
build_values_join_select(&probe).map_err(DjogiError::from)?,
"join_values::fetch_one",
)?
.into_parts();
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
match rows.len() {
0 => Err(DjogiError::not_found(T::table_name())),
1 => decode_inner_pair::<T, Row>(&rows[0]),
n => Err(DjogiError::multiple_objects(T::table_name(), n)),
}
}
}
pub fn count<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<i64, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "join_values::count")?;
validate_values_on(&self.on, "join_values::count")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(0);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_values_join_count(&self).map_err(DjogiError::from)?,
"join_values::count",
)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
row.try_get::<_, i64>(0)
.map_err(|e| DjogiError::Decode(format!("join_values count: {e}")))
}
}
pub fn exists<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<bool, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "join_values::exists")?;
validate_values_on(&self.on, "join_values::exists")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(false);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_values_join_exists(&self).map_err(DjogiError::from)?,
"join_values::exists",
)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
row.try_get::<_, bool>(0)
.map_err(|e| DjogiError::Decode(format!("join_values exists: {e}")))
}
}
}
impl<T, Row> LeftValuesJoinedQuerySet<T, Row>
where
T: Model + FromPgRow + Send + Unpin,
Row: ValuesRow + Send + Unpin,
{
pub fn fetch_all<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Vec<(T, Option<Row>)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "left_join_values::fetch_all")?;
validate_values_on(&self.on, "left_join_values::fetch_all")?;
if self.left.is_empty() {
return Ok(vec![]);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let col_count = <T as FromPgRow>::COLUMNS.len();
let (sql, binds) = validate_total_bind_count(
(if self.values.is_empty() {
build_left_values_join_empty_select(&self)
} else {
build_left_values_join_select(&self)
})
.map_err(DjogiError::from)?,
"left_join_values::fetch_all",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_all(&sql, ¶ms)
.await?
.iter()
.map(|r| decode_left_pair::<T, Row>(r, col_count))
.collect()
}
}
pub fn first<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Option<(T, Option<Row>)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "left_join_values::first")?;
validate_values_on(&self.on, "left_join_values::first")?;
if self.left.is_empty() {
return Ok(None);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let col_count = <T as FromPgRow>::COLUMNS.len();
let mut limited = self;
limited.left.limit = Some(1);
let (sql, binds) = validate_total_bind_count(
(if limited.values.is_empty() {
build_left_values_join_empty_select(&limited)
} else {
build_left_values_join_select(&limited)
})
.map_err(DjogiError::from)?,
"left_join_values::first",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_opt(&sql, ¶ms)
.await?
.as_ref()
.map(|r| decode_left_pair::<T, Row>(r, col_count))
.transpose()
}
}
pub fn fetch_one<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<(T, Option<Row>), DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "left_join_values::fetch_one")?;
validate_values_on(&self.on, "left_join_values::fetch_one")?;
if self.left.is_empty() {
return Err(DjogiError::not_found(T::table_name()));
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let col_count = <T as FromPgRow>::COLUMNS.len();
let mut probe = self;
probe.left.limit = Some(2);
let (sql, binds) = validate_total_bind_count(
(if probe.values.is_empty() {
build_left_values_join_empty_select(&probe)
} else {
build_left_values_join_select(&probe)
})
.map_err(DjogiError::from)?,
"left_join_values::fetch_one",
)?
.into_parts();
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
match rows.len() {
0 => Err(DjogiError::not_found(T::table_name())),
1 => decode_left_pair::<T, Row>(&rows[0], col_count),
n => Err(DjogiError::multiple_objects(T::table_name(), n)),
}
}
}
pub fn count<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<i64, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "left_join_values::count")?;
validate_values_on(&self.on, "left_join_values::count")?;
if self.left.is_empty() {
return Ok(0);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_left_values_join_count(&self).map_err(DjogiError::from)?,
"left_join_values::count",
)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
row.try_get::<_, i64>(0)
.map_err(|e| DjogiError::Decode(format!("left_join_values count: {e}")))
}
}
pub fn exists<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<bool, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "left_join_values::exists")?;
validate_values_on(&self.on, "left_join_values::exists")?;
if self.left.is_empty() {
return Ok(false);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_left_values_join_exists(&self).map_err(DjogiError::from)?,
"left_join_values::exists",
)?
.into_parts();
let params = as_params(&binds);
let exists = ctx
.query_one(&sql, ¶ms)
.await?
.try_get::<_, bool>(0)
.map_err(|e| DjogiError::Decode(format!("left_join_values exists: {e}")))?;
Ok(exists)
}
}
}
impl<T, Row> CrossValuesJoinedQuerySet<T, Row>
where
T: Model + FromPgRow + Send + Unpin,
Row: ValuesRow + Send + Unpin,
{
pub fn fetch_all<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Vec<(T, Row)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "cross_join_values::fetch_all")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(vec![]);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_cross_values_join_select(&self).map_err(DjogiError::from)?,
"cross_join_values::fetch_all",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_all(&sql, ¶ms)
.await?
.iter()
.map(|r| decode_inner_pair::<T, Row>(r))
.collect()
}
}
pub fn first<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Option<(T, Row)>, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "cross_join_values::first")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(None);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let mut limited = self;
limited.left.limit = Some(1);
let (sql, binds) = validate_total_bind_count(
build_cross_values_join_select(&limited).map_err(DjogiError::from)?,
"cross_join_values::first",
)?
.into_parts();
let params = as_params(&binds);
ctx.query_opt(&sql, ¶ms)
.await?
.as_ref()
.map(|r| decode_inner_pair::<T, Row>(r))
.transpose()
}
}
pub fn fetch_one<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<(T, Row), DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "cross_join_values::fetch_one")?;
if self.left.is_empty() || self.values.is_empty() {
return Err(DjogiError::not_found(T::table_name()));
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let mut probe = self;
probe.left.limit = Some(2);
let (sql, binds) = validate_total_bind_count(
build_cross_values_join_select(&probe).map_err(DjogiError::from)?,
"cross_join_values::fetch_one",
)?
.into_parts();
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
match rows.len() {
0 => Err(DjogiError::not_found(T::table_name())),
1 => decode_inner_pair::<T, Row>(&rows[0]),
n => Err(DjogiError::multiple_objects(T::table_name(), n)),
}
}
}
pub fn count<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<i64, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "cross_join_values::count")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(0);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_cross_values_join_count(&self).map_err(DjogiError::from)?,
"cross_join_values::count",
)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
row.try_get::<_, i64>(0)
.map_err(|e| DjogiError::Decode(format!("cross_join_values count: {e}")))
}
}
pub fn exists<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<bool, DjogiError>> + Send + 'ctx
where
T: 'ctx,
Row: 'ctx,
{
async move {
validate_left_qs(&self.left, "cross_join_values::exists")?;
if self.left.is_empty() || self.values.is_empty() {
return Ok(false);
}
crate::query::terminal::auto_set_tenant::<T>(ctx).await?;
let (sql, binds) = validate_total_bind_count(
build_cross_values_join_exists(&self).map_err(DjogiError::from)?,
"cross_join_values::exists",
)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
row.try_get::<_, bool>(0)
.map_err(|e| DjogiError::Decode(format!("cross_join_values exists: {e}")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ModelDescriptor;
struct Stub;
impl crate::model::__sealed::Sealed for Stub {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for Stub {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"stub_table"
}
fn pk_value(&self) -> &i64 {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut DjogiContext,
_id: i64,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut DjogiContext,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
}
impl FromPgRow for Stub {
const COLUMNS: &'static [&'static str] = &["id"];
const COLUMN_LIST: &'static str = "id";
fn from_pg_row(_row: &tokio_postgres::Row) -> Result<Self, DjogiError> {
unreachable!()
}
}
#[test]
fn inline_values_accepts_tuple_rows_and_static_columns() {
let iv: InlineValues<(i64, f64)> = InlineValues::new(
vec![(1_i64, 0.91_f64), (2_i64, 0.72_f64)],
"weights",
("animal_id", "score"),
)
.expect("should succeed");
assert_eq!(iv.rows.len(), 2);
assert_eq!(iv.alias, "weights");
assert_eq!(iv.columns, vec!["animal_id", "score"]);
assert!(!iv.is_empty());
}
#[test]
fn inline_values_accepts_empty_rows() {
let iv: InlineValues<(i64, f64)> =
InlineValues::new(vec![], "scores", ("id", "score")).expect("empty rows are valid");
assert!(iv.is_empty());
assert_eq!(iv.len(), 0);
}
#[test]
fn inline_values_rejects_bad_alias() {
let err = InlineValues::<(i64,)>::new(vec![(1_i64,)], "bad alias", ("col",)).unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation");
};
assert!(msg.contains("bad alias"), "got: {msg}");
}
#[test]
fn inline_values_rejects_bad_column() {
let err = InlineValues::<(i64,)>::new(vec![(1_i64,)], "alias", ("bad col",)).unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("bad col"), "got: {msg}");
}
#[test]
fn inline_values_rejects_reserved_djogi_prefix() {
let err =
InlineValues::<(i64,)>::new(vec![(1_i64,)], "__djogi_scores", ("col",)).unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("__djogi_scores"), "got: {msg}");
}
#[test]
fn inline_values_rejects_reserved_djogi_prefix_column() {
let err = InlineValues::<(i64,)>::new(vec![(1_i64,)], "weights", ("__djogi_internal",))
.unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("__djogi_internal"), "got: {msg}");
}
#[test]
fn inline_values_rejects_duplicate_columns() {
let err =
InlineValues::<(i64, f64)>::new(vec![(1_i64, 0.5_f64)], "weights", ("score", "score"))
.unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("score"), "got: {msg}");
}
#[test]
fn inline_values_rejects_case_folded_duplicate_columns() {
let err =
InlineValues::<(i64, f64)>::new(vec![(1_i64, 0.5_f64)], "weights", ("Score", "score"))
.unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("Score") || msg.contains("score"), "got: {msg}");
}
#[test]
fn inline_values_rejects_parameter_count_overflow() {
let rows: Vec<(i64,)> = vec![(0_i64,); PG_MAX_PARAMS + 1];
let err = InlineValues::new(rows, "t", ("col",)).unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(
msg.contains("65535") || msg.contains("65536") || msg.contains("param"),
"got: {msg}"
);
}
#[test]
fn values_on_rejects_relation_path_model_columns() {
let err = validate_values_on(
&(ValuesOn::<Stub>::eq("id", 0) & ValuesOn::<Stub>::eq("department.name", 1)),
"join_values::fetch_all",
)
.unwrap_err();
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(msg.contains("department.name"), "got: {msg}");
assert!(msg.contains("root-model columns"), "got: {msg}");
}
fn stub_qs() -> QuerySet<Stub> {
QuerySet::new()
}
fn stub_vqs(alias: &str) -> ValuesJoinedQuerySet<Stub, (i64, f64)> {
let values = InlineValues::new(vec![(1_i64, 0.5_f64)], alias, ("aid", "sc")).unwrap();
ValuesJoinedQuerySet {
left: stub_qs(),
values,
on: ValuesOn::eq("id", 0),
}
}
#[test]
fn values_join_sql_projects_model_then_values_columns() {
let sql = build_values_join_select(&stub_vqs("w")).unwrap();
let s = sql.sql().to_owned();
assert!(
s.starts_with("SELECT __djogi_m.id AS id"),
"model columns first; sql = {s}"
);
assert!(
s.contains("w.aid AS __djogi_values_0"),
"values col 0; sql = {s}"
);
assert!(
s.contains("w.sc AS __djogi_values_1"),
"values col 1; sql = {s}"
);
}
#[test]
fn values_join_sql_casts_first_row_placeholders() {
let values =
InlineValues::new(vec![(1_i64, 0.5_f64), (2_i64, 0.8_f64)], "w", ("aid", "sc"))
.unwrap();
let vqs = ValuesJoinedQuerySet {
left: stub_qs(),
values,
on: ValuesOn::eq("id", 0),
};
let acc = build_values_join_select(&vqs).unwrap();
let s = acc.sql().to_owned();
assert!(
s.contains("$1::BIGINT") && s.contains("$2::DOUBLE PRECISION"),
"first row has casts; sql = {s}"
);
assert!(
s.contains(", ($3, $4)"),
"second row bare params; sql = {s}"
);
}
#[test]
fn values_join_sql_uses_lexical_bind_order() {
use crate::query::condition::{Condition, FilterValue, Leaf};
let values = InlineValues::new(vec![(99_i64, 1.0_f64)], "w", ("aid", "sc")).unwrap();
let qs = stub_qs().filter(|_| Condition::Leaf(Leaf::eq_raw("id", FilterValue::I64(42))));
let vqs = ValuesJoinedQuerySet {
left: qs,
values,
on: ValuesOn::eq("id", 0),
};
let acc = build_values_join_select(&vqs).unwrap();
let s = acc.sql().to_owned();
let pos_values = s.find("$1::BIGINT").expect("VALUES bind first");
let pos_where = s.rfind("$3").expect("WHERE bind present");
assert!(pos_values < pos_where, "VALUES before WHERE; sql = {s}");
assert_eq!(acc.bind_count(), 3, "2 VALUES + 1 WHERE; sql = {s}");
}
#[test]
fn values_join_first_rejects_total_bind_count_past_pg_limit() {
let values = InlineValues::new(vec![(1_i64,); PG_MAX_PARAMS], "w", ("aid",))
.expect("raw VALUES ceiling is allowed at construction");
let mut qs = stub_qs();
qs.limit = Some(1);
let vqs = ValuesJoinedQuerySet {
left: qs,
values,
on: ValuesOn::eq("id", 0),
};
let err = match validate_total_bind_count(
build_values_join_select(&vqs).unwrap(),
"join_values::first",
) {
Ok(_) => panic!("expected Validation"),
Err(err) => err,
};
let DjogiError::Validation(msg) = err else {
panic!("expected Validation")
};
assert!(
msg.contains("65535") || msg.contains("65536") || msg.contains("bind"),
"got: {msg}"
);
}
#[test]
fn values_join_sql_emits_structured_on_not_on_true() {
let acc = build_values_join_select(&stub_vqs("w")).unwrap();
let s = acc.sql().to_owned();
assert!(
s.contains("ON __djogi_m.id = w.aid"),
"structured ON; sql = {s}"
);
assert!(!s.contains("ON TRUE"), "no ON TRUE; sql = {s}");
}
#[test]
fn values_join_sql_emits_parenthesized_compound_on_predicate() {
let values =
InlineValues::new(vec![(1_i64, 10_i32)], "w", ("aid", "score")).expect("valid");
let vqs = ValuesJoinedQuerySet {
left: stub_qs(),
values,
on: ValuesOn::eq("id", 0) & ValuesOn::eq("score", 1),
};
let acc = build_values_join_select(&vqs).unwrap();
let s = acc.sql().to_owned();
assert!(
s.contains("ON (__djogi_m.id = w.aid AND __djogi_m.score = w.score)"),
"compound ON must be parenthesized and qualified; sql = {s}"
);
}
#[test]
fn inline_values_empty_is_valid_construction() {
let iv: InlineValues<(i64,)> =
InlineValues::new(vec![], "w", ("id",)).expect("empty is valid");
assert!(iv.is_empty());
}
fn stub_cross_vqs(alias: &str) -> CrossValuesJoinedQuerySet<Stub, (i64, f64)> {
let values = InlineValues::new(vec![(1_i64, 0.5_f64)], alias, ("aid", "sc")).unwrap();
CrossValuesJoinedQuerySet {
left: stub_qs(),
values,
}
}
#[test]
fn cross_join_sql_projects_model_then_values_columns() {
let acc = build_cross_values_join_select(&stub_cross_vqs("labels")).unwrap();
let s = acc.sql().to_owned();
assert!(
s.starts_with("SELECT __djogi_m.id AS id"),
"model columns first; sql = {s}"
);
assert!(
s.contains("labels.aid AS __djogi_values_0"),
"values col 0; sql = {s}"
);
assert!(
s.contains("labels.sc AS __djogi_values_1"),
"values col 1; sql = {s}"
);
}
#[test]
fn cross_join_sql_uses_cross_join_keyword_not_inner_or_left() {
let acc = build_cross_values_join_select(&stub_cross_vqs("lbl")).unwrap();
let s = acc.sql().to_owned();
assert!(s.contains("CROSS JOIN"), "must use CROSS JOIN; sql = {s}");
assert!(
!s.contains("INNER JOIN"),
"must not have INNER JOIN; sql = {s}"
);
assert!(
!s.contains("LEFT JOIN"),
"must not have LEFT JOIN; sql = {s}"
);
assert!(
!s.contains(" ON "),
"no ON predicate for cross join; sql = {s}"
);
}
#[test]
fn cross_join_sql_has_no_sentinel_column() {
let acc = build_cross_values_join_select(&stub_cross_vqs("lbl")).unwrap();
let s = acc.sql().to_owned();
assert!(
!s.contains("__djogi_present"),
"no sentinel for cross join; sql = {s}"
);
}
#[test]
fn cross_join_sql_casts_first_row_placeholders() {
let values = InlineValues::new(
vec![(1_i64, 0.5_f64), (2_i64, 0.8_f64)],
"lbl",
("aid", "sc"),
)
.unwrap();
let cqs = CrossValuesJoinedQuerySet {
left: stub_qs(),
values,
};
let acc = build_cross_values_join_select(&cqs).unwrap();
let s = acc.sql().to_owned();
assert!(
s.contains("$1::BIGINT") && s.contains("$2::DOUBLE PRECISION"),
"first row has casts; sql = {s}"
);
assert!(
s.contains(", ($3, $4)"),
"second row bare params; sql = {s}"
);
}
#[test]
fn cross_join_count_sql_uses_cross_join() {
let acc = build_cross_values_join_count(&stub_cross_vqs("lbl")).unwrap();
let s = acc.sql().to_owned();
assert!(
s.starts_with("SELECT COUNT(*) FROM"),
"starts with COUNT(*); sql = {s}"
);
assert!(s.contains("CROSS JOIN"), "count uses CROSS JOIN; sql = {s}");
assert!(!s.contains(" ON "), "no ON in count; sql = {s}");
}
#[test]
fn cross_join_exists_sql_uses_cross_join() {
let acc = build_cross_values_join_exists(&stub_cross_vqs("lbl")).unwrap();
let s = acc.sql().to_owned();
assert!(
s.starts_with("SELECT EXISTS"),
"starts with EXISTS; sql = {s}"
);
assert!(
s.contains("CROSS JOIN"),
"exists uses CROSS JOIN; sql = {s}"
);
assert!(!s.contains(" ON "), "no ON in exists; sql = {s}");
}
#[test]
fn cross_join_sql_uses_lexical_bind_order() {
use crate::query::condition::{Condition, FilterValue, Leaf};
let values = InlineValues::new(vec![(99_i64, 1.0_f64)], "lbl", ("aid", "sc")).unwrap();
let qs = stub_qs().filter(|_| Condition::Leaf(Leaf::eq_raw("id", FilterValue::I64(42))));
let cqs = CrossValuesJoinedQuerySet { left: qs, values };
let acc = build_cross_values_join_select(&cqs).unwrap();
let s = acc.sql().to_owned();
let pos_values = s.find("$1::BIGINT").expect("VALUES bind first");
let pos_where = s.rfind("$3").expect("WHERE bind present");
assert!(pos_values < pos_where, "VALUES before WHERE; sql = {s}");
assert_eq!(acc.bind_count(), 3, "2 VALUES + 1 WHERE; sql = {s}");
}
#[test]
fn left_join_exists_sql_uses_left_queryset_only() {
use crate::query::condition::{Condition, FilterValue, Leaf};
let values = InlineValues::new(
vec![(99_i64, 1.0_f64), (100_i64, 2.0_f64)],
"w",
("aid", "sc"),
)
.unwrap();
let qs = stub_qs().filter(|_| Condition::Leaf(Leaf::eq_raw("id", FilterValue::I64(42))));
let vqs = LeftValuesJoinedQuerySet {
left: qs,
values,
on: ValuesOn::eq("id", 0),
};
let acc = build_left_values_join_exists(&vqs).unwrap();
let s = acc.sql().to_owned();
assert!(
s.starts_with("SELECT EXISTS"),
"exists must use SELECT EXISTS; sql = {s}"
);
assert!(!s.contains("COUNT(*)"), "exists must not count; sql = {s}");
assert!(
!s.contains("LEFT JOIN"),
"exists must not join values; sql = {s}"
);
assert!(
!s.contains("(VALUES"),
"exists must not bind values rows; sql = {s}"
);
assert_eq!(acc.bind_count(), 1, "only WHERE bind expected; sql = {s}");
}
#[test]
fn left_join_empty_values_sql_uses_typed_zero_row_relation_join() {
let values: InlineValues<(i64, f64)> =
InlineValues::new(vec![], "w", ("aid", "sc")).unwrap();
let vqs = LeftValuesJoinedQuerySet {
left: stub_qs(),
values,
on: ValuesOn::eq("id", 0),
};
let acc = build_left_values_join_empty_select(&vqs).unwrap();
let s = acc.sql().to_owned();
assert!(
s.contains("LEFT JOIN (SELECT"),
"must emit a LEFT JOIN with typed subquery; sql = {s}"
);
assert!(
s.contains("NULL::BIGINT AS aid"),
"typed null col0 inside subquery; sql = {s}"
);
assert!(
s.contains("NULL::DOUBLE PRECISION AS sc"),
"typed null col1 inside subquery; sql = {s}"
);
assert!(
s.contains("NULL::BOOLEAN AS __djogi_present"),
"typed null sentinel inside subquery; sql = {s}"
);
assert!(s.contains("WHERE 1=0"), "zero-row guard; sql = {s}");
assert!(
s.contains(") AS w"),
"subquery aliased to user alias; sql = {s}"
);
assert!(
s.contains("ON __djogi_m.id = w.aid"),
"ON predicate present; sql = {s}"
);
assert!(
s.contains("w.aid AS __djogi_values_0"),
"outer projection references alias; sql = {s}"
);
assert!(
s.contains("w.__djogi_present AS __djogi_values_present"),
"sentinel from alias; sql = {s}"
);
assert_eq!(
acc.bind_count(),
0,
"no binds for empty left join; sql = {s}"
);
}
}