use crate::expr::Expr;
use crate::expr::node::{ErasedSubqueryPredicate, ExprNode, SubqueryNode};
use crate::model::Model;
use crate::query::queryset::QuerySet;
use std::any::TypeId;
use std::marker::PhantomData;
#[must_use = "subqueries are lazy — drop one and the predicate is silently omitted"]
pub struct Subquery<T: Model, V> {
node: SubqueryNode,
_phantom: PhantomData<fn() -> (T, V)>,
}
impl<T: Model, V> Subquery<T, V> {
pub fn new<S>(qs: QuerySet<T>, column: S) -> Self
where
S: crate::query::field::IntoSqlField<T, V>,
{
Subquery {
node: SubqueryNode {
table: T::table_name(),
select_column: Some(column.into_sql_field().column()),
where_clause: q_to_subquery_opt::<T>(qs.condition),
},
_phantom: PhantomData,
}
}
pub fn as_expr(self) -> Expr<V> {
Expr::from_node(ExprNode::Subquery(Box::new(self.node)))
}
}
#[must_use = "EXISTS predicates are lazy — drop one and the filter is silently omitted"]
pub struct Exists {
node: SubqueryNode,
}
impl Exists {
pub fn new<T: Model>(qs: QuerySet<T>) -> Self {
Exists {
node: SubqueryNode {
table: T::table_name(),
select_column: None,
where_clause: q_to_subquery_opt::<T>(qs.condition),
},
}
}
pub fn as_expr(self) -> Expr<bool> {
Expr::from_node(ExprNode::Exists(Box::new(self.node)))
}
}
pub struct OuterRef<M: Model, V> {
column: &'static str,
_m: PhantomData<fn() -> M>,
_v: PhantomData<fn() -> V>,
}
impl<M: Model, V> Copy for OuterRef<M, V> {}
impl<M: Model, V> Clone for OuterRef<M, V> {
fn clone(&self) -> Self {
*self
}
}
impl<M: Model, V> std::fmt::Debug for OuterRef<M, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OuterRef({})", self.column)
}
}
impl<M: Model, V> OuterRef<M, V> {
pub(crate) const fn new(column: &'static str) -> Self {
OuterRef {
column,
_m: PhantomData,
_v: PhantomData,
}
}
#[must_use = "OuterRef is inert unless promoted to Expr<V>"]
pub fn as_expr(self) -> Expr<V> {
Expr::from_node(ExprNode::OuterRef {
column: self.column,
})
}
#[must_use = "OuterRef is inert unless promoted to Expr<V>"]
pub fn as_qualified_expr(self) -> Expr<V> {
Expr::from_node(ExprNode::OuterRefColumn {
table: M::table_name(),
column: self.column,
})
}
#[must_use = "OuterRef is inert unless promoted to Expr<V>"]
pub fn as_lateral_outer_expr(self) -> Expr<V> {
Expr::from_node(ExprNode::OuterRefAlias {
alias: "l",
column: self.column,
model_type: TypeId::of::<M>(),
model_name: std::any::type_name::<M>(),
})
}
}
fn q_to_subquery_opt<T: Model>(q: crate::query::q::Q<T>) -> Option<ErasedSubqueryPredicate> {
if is_vacuously_true(&q) {
None
} else {
Some(ErasedSubqueryPredicate::from_q::<T>(q))
}
}
fn is_vacuously_true<T: Model>(q: &crate::query::q::Q<T>) -> bool {
use crate::query::q::{CompoundOp, Q};
match q {
Q::Portable(p) => match p.inner_ref() {
sassi::BasicPredicate::True => true,
sassi::BasicPredicate::And(parts) => parts
.iter()
.all(|c| matches!(c, sassi::BasicPredicate::True)),
_ => false,
},
Q::Condition(c) => c.is_vacuously_true(),
Q::Compound {
op: CompoundOp::And,
parts,
} => parts.iter().all(is_vacuously_true),
Q::Negated(inner) => match inner.as_ref() {
Q::Portable(p) => matches!(p.inner_ref(), sassi::BasicPredicate::False),
Q::Compound {
op: CompoundOp::Or,
parts,
} => parts.is_empty(),
_ => false,
},
_ => false,
}
}
#[doc(hidden)]
pub mod __macro_support {
use super::OuterRef;
use crate::ident::assert_plain_ident;
use crate::model::Model;
#[doc(hidden)]
pub fn __make_outer_ref<M: Model, V>(column: &'static str) -> OuterRef<M, V> {
assert_plain_ident(column, "outer_ref_column");
OuterRef::new(column)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Expr;
use crate::descriptor::ModelDescriptor;
use crate::expr::sql::emit_expr;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::portable::SqlEmitContext;
struct Ledger;
impl crate::model::__sealed::Sealed for Ledger {}
#[allow(clippy::manual_async_fn)]
impl Model for Ledger {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"ledgers"
}
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 Entry {
id: i64,
memo: String,
active: bool,
}
impl crate::model::__sealed::Sealed for Entry {}
#[allow(clippy::manual_async_fn)]
impl Model for Entry {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"entries"
}
fn pk_value(&self) -> &i64 {
&self.id
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn __djogi_emit_field_predicate(
acc: &mut crate::pg::accumulator::SqlAccumulator,
field: &crate::types::FieldPredicate<Self>,
ctx: crate::query::SqlEmitContext,
) -> Result<(), crate::query::PortablePredicateError> {
use crate::query::portable::emit;
use crate::types::LookupOp;
match (field.field_name(), field.op()) {
("memo", LookupOp::Eq) => {
emit::emit_value::<Self, String>(acc, ctx, "memo", " = ", field)
}
("active", LookupOp::Eq) => {
emit::emit_value::<Self, bool>(acc, ctx, "active", " = ", field)
}
("memo", op) => Err(crate::query::PortablePredicateError::UnsupportedLookup {
field: "memo",
op,
}),
("active", op) => Err(crate::query::PortablePredicateError::UnsupportedLookup {
field: "active",
op,
}),
(field, _) => Err(crate::query::PortablePredicateError::UnsupportedField { field }),
}
}
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 exists_no_filter_emits_bare_select_one() {
let qs: QuerySet<Entry> = QuerySet::new();
let expr = Exists::new(qs).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("subquery emission");
let sql = qb.sql();
assert_eq!(sql.trim(), "EXISTS (SELECT 1 FROM entries)", "got: {sql}");
}
#[test]
fn scalar_subquery_no_filter_emits_without_where() {
use crate::query::field::FieldRef;
let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
let qs: QuerySet<Entry> = QuerySet::new();
let expr = Subquery::new(qs, id_col).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
let sql = qb.sql();
assert_eq!(sql.trim(), "(SELECT id FROM entries)", "got: {sql}");
}
#[test]
fn exists_with_portable_predicate_uses_erased_q_emitter() {
use crate::query::field::djogi_field_macro_support::__make_djogi_field;
let active = __make_djogi_field::<Entry, bool>("active", |row| &row.active);
let qs: QuerySet<Entry> = QuerySet::new().filter_struct(active.eq(true));
let expr = Exists::new(qs).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
let sql = qb.sql();
assert_eq!(
sql.trim(),
"EXISTS (SELECT 1 FROM entries WHERE active = $1)",
"got: {sql}"
);
}
#[test]
fn exists_under_joined_context_keeps_subquery_fields_unqualified() {
use crate::query::field::djogi_field_macro_support::__make_djogi_field;
let active = __make_djogi_field::<Entry, bool>("active", |row| &row.active);
let qs: QuerySet<Entry> = QuerySet::new().filter_struct(active.eq(true));
let expr = Exists::new(qs).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::joined("l")).expect("expression emission");
let sql = qb.sql();
assert_eq!(
sql.trim(),
"EXISTS (SELECT 1 FROM entries WHERE active = $1)",
"subquery-owned fields must not inherit the outer joined alias: {sql}"
);
}
#[test]
fn exists_with_correlated_outer_ref() {
use crate::query::field::FieldRef;
let inner_col: FieldRef<Entry, i64> = FieldRef::new("ledger_id");
let outer_ref: OuterRef<Ledger, i64> = OuterRef::new("id");
let qs: QuerySet<Entry> =
QuerySet::new().filter_expr(|_| inner_col.as_expr().eq(outer_ref.as_expr()));
let expr = Exists::new(qs).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
let sql = qb.sql();
assert_eq!(
sql.trim(),
"EXISTS (SELECT 1 FROM entries WHERE ledger_id = id)",
"got: {sql}"
);
}
#[test]
fn scalar_subquery_emits_select_col() {
use crate::query::field::FieldRef;
let memo: FieldRef<Entry, String> = FieldRef::new("memo");
let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
let qs: QuerySet<Entry> = QuerySet::new().filter(|_| memo.eq("opening".to_string()));
let expr = Subquery::new(qs, id_col).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
let sql = qb.sql();
assert_eq!(
sql.trim(),
"(SELECT id FROM entries WHERE memo = $1)",
"got: {sql}"
);
}
#[test]
fn scalar_subquery_with_portable_predicate_uses_erased_q_emitter() {
use crate::query::field::FieldRef;
use crate::query::field::djogi_field_macro_support::__make_djogi_field;
let memo = __make_djogi_field::<Entry, String>("memo", |row| &row.memo);
let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
let qs: QuerySet<Entry> = QuerySet::new().filter_struct(memo.eq("opening".to_string()));
let expr = Subquery::new(qs, id_col).as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
let sql = qb.sql();
assert_eq!(
sql.trim(),
"(SELECT id FROM entries WHERE memo = $1)",
"got: {sql}"
);
}
#[test]
fn outer_ref_emits_bare_column() {
let r: OuterRef<Ledger, i64> = OuterRef::new("id");
let expr: Expr<i64> = r.as_expr();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
assert_eq!(qb.sql().trim(), "id", "got: {}", qb.sql());
}
#[test]
fn make_outer_ref_seal_validates_identifier() {
let result =
std::panic::catch_unwind(|| __macro_support::__make_outer_ref::<Ledger, i64>("id"));
assert!(result.is_ok(), "plain column name must pass the seal");
let result =
std::panic::catch_unwind(|| __macro_support::__make_outer_ref::<Ledger, i64>("1bad"));
assert!(result.is_err(), "leading-digit column must panic the seal");
let result = std::panic::catch_unwind(|| {
__macro_support::__make_outer_ref::<Ledger, i64>("id) OR 1=1 --")
});
assert!(result.is_err(), "SQL-metachar payload must panic the seal");
}
}