#![allow(clippy::manual_async_fn)]
use crate::DjogiError;
use crate::context::DjogiContext;
use crate::pg::accumulator::{SqlAccumulator, as_params};
use crate::pg::decode::{FromPgRow, try_get_scalar};
use crate::query::order::OrderExpr;
use crate::query::portable::SqlEmitContext;
use crate::query::sql::{emit_q, q_is_vacuously_true};
use crate::query::{IntoQ, Q};
use crate::visage::DjogiVisage;
use postgres_types::ToSql;
use std::future::Future;
use std::marker::PhantomData;
pub struct VisageQuerySet<V: DjogiVisage> {
pub(crate) table: &'static str,
pub(crate) projection_list: &'static str,
pub(crate) condition: Q<V::Model>,
pub(crate) ordering: Vec<OrderExpr>,
pub(crate) limit: Option<i64>,
pub(crate) offset: Option<i64>,
_visage: PhantomData<fn() -> V>,
}
#[must_use = "visage columns are inert until projected through selecting()"]
pub struct VisageColumn<V: DjogiVisage, U> {
pub(crate) column: &'static str,
__sealed: crate::visage::VisageColumnToken,
_v: PhantomData<fn() -> V>,
_u: PhantomData<fn() -> U>,
}
impl<V: DjogiVisage, U> Clone for VisageColumn<V, U> {
fn clone(&self) -> Self {
*self
}
}
impl<V: DjogiVisage, U> Copy for VisageColumn<V, U> {}
impl<V: DjogiVisage, U> std::fmt::Debug for VisageColumn<V, U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VisageColumn")
.field("column", &self.column)
.finish()
}
}
impl<V: DjogiVisage, U> VisageColumn<V, U> {
#[doc(hidden)]
pub fn __new_for_visage_column(
column: &'static str,
token: crate::visage::VisageColumnToken,
) -> Self {
crate::ident::assert_plain_ident(column, "visage_column");
assert!(
<V as crate::visage::DjogiVisage>::COLUMNS.contains(&column),
"djogi::query::VisageColumn: column {column:?} is not a member of visage `{}`'s exposed COLUMNS",
std::any::type_name::<V>(),
);
Self {
column,
__sealed: token,
_v: PhantomData,
_u: PhantomData,
}
}
}
#[must_use = "a VisageSubquery is lazy — embed it in a field predicate or it has no effect"]
pub struct VisageSubquery<V: DjogiVisage, U> {
table: &'static str,
select_column: &'static str,
condition: Q<V::Model>,
_u: PhantomData<fn() -> U>,
}
impl<V: DjogiVisage, U> std::fmt::Debug for VisageSubquery<V, U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VisageSubquery")
.field("table", &self.table)
.field("select_column", &self.select_column)
.field("condition", &self.condition)
.finish()
}
}
#[must_use = "EXISTS predicates are lazy — drop one and the filter is silently omitted"]
#[derive(Debug, Clone)]
pub struct VisageExists {
node: crate::expr::node::SubqueryNode,
}
impl VisageExists {
pub fn new<V: DjogiVisage>(qs: VisageQuerySet<V>) -> Result<Self, DjogiError> {
reject_subquery_modifiers("VisageExists::new", &qs.ordering, qs.limit, qs.offset)?;
Ok(Self {
node: crate::expr::node::SubqueryNode {
table: qs.table,
select_column: None,
where_clause: crate::expr::subquery::__q_to_subquery_opt::<V::Model>(qs.condition),
},
})
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn as_expr(self) -> crate::expr::Expr<bool> {
crate::expr::Expr::from_node(crate::expr::node::ExprNode::Exists(Box::new(self.node)))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn not_exists(self) -> crate::expr::Expr<bool> {
let expr = self.as_expr();
crate::expr::Expr::from_node(crate::expr::node::ExprNode::Not(Box::new(expr.node)))
}
}
impl<V: DjogiVisage, U> VisageSubquery<V, U> {
#[doc(hidden)]
pub(crate) fn __into_subquery_node(self) -> crate::expr::node::SubqueryNode {
crate::expr::node::SubqueryNode {
table: self.table,
select_column: Some(self.select_column),
where_clause: crate::expr::subquery::__q_to_subquery_opt::<V::Model>(self.condition),
}
}
}
impl<V: DjogiVisage> Clone for VisageQuerySet<V> {
fn clone(&self) -> Self {
VisageQuerySet {
table: self.table,
projection_list: self.projection_list,
condition: self.condition.clone(),
ordering: self.ordering.clone(),
limit: self.limit,
offset: self.offset,
_visage: PhantomData,
}
}
}
impl<V: DjogiVisage> std::fmt::Debug for VisageQuerySet<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VisageQuerySet")
.field("table", &self.table)
.field("projection_list", &self.projection_list)
.field("condition", &self.condition)
.field("ordering", &self.ordering)
.field("limit", &self.limit)
.field("offset", &self.offset)
.finish()
}
}
impl<V: DjogiVisage> VisageQuerySet<V> {
#[doc(hidden)]
#[must_use = "querysets are lazy — dropping one silently omits the query"]
pub fn new_for_visage(table: &'static str, projection_list: &'static str) -> Self {
VisageQuerySet {
table,
projection_list,
condition: Q::<V::Model>::always_true(),
ordering: Vec::new(),
limit: None,
offset: None,
_visage: PhantomData,
}
}
#[must_use = "querysets are lazy — dropping one silently omits the query"]
pub fn filter<P>(mut self, cond: P) -> Self
where
P: IntoQ<V::Model>,
{
self.condition = and_q_into_q(self.condition, cond);
self
}
#[must_use = "querysets are lazy — dropping one silently omits the query"]
pub fn order_by<I: Into<Vec<OrderExpr>>>(mut self, orderings: I) -> Self {
self.ordering.extend(orderings.into());
self
}
#[must_use = "querysets are lazy — dropping one silently omits the query"]
pub fn limit(mut self, n: u64) -> Self {
let n = i64::try_from(n)
.unwrap_or_else(|_| panic!("VisageQuerySet::limit(n = {n}) overflows i64"));
self.limit = Some(n);
self
}
#[must_use = "querysets are lazy — dropping one silently omits the query"]
pub fn offset(mut self, n: u64) -> Self {
let n = i64::try_from(n)
.unwrap_or_else(|_| panic!("VisageQuerySet::offset(n = {n}) overflows i64"));
self.offset = Some(n);
self
}
pub fn selecting<U>(self, col: VisageColumn<V, U>) -> Result<VisageSubquery<V, U>, DjogiError> {
reject_subquery_modifiers("selecting", &self.ordering, self.limit, self.offset)?;
Ok(VisageSubquery {
table: self.table,
select_column: col.column,
condition: self.condition,
_u: PhantomData,
})
}
#[doc(hidden)]
pub fn __sql_for_test(&self) -> String {
let acc = build_visage_select(self).expect("visage select");
let (sql, _binds) = acc.into_parts();
sql
}
}
fn run_all_sql<V: DjogiVisage>(
qs: &VisageQuerySet<V>,
) -> Result<(String, Vec<Box<dyn ToSql + Sync + Send>>), crate::DjogiError> {
Ok(build_visage_select(qs)
.map_err(crate::DjogiError::from)?
.into_parts())
}
pub(crate) fn build_visage_select<V: DjogiVisage>(
qs: &VisageQuerySet<V>,
) -> Result<SqlAccumulator, crate::query::portable::PortablePredicateError> {
let mut acc = SqlAccumulator::new("");
acc.push_sql("SELECT ");
acc.push_sql(qs.projection_list);
acc.push_sql(" FROM ");
acc.push_sql(qs.table);
push_visage_tail(&mut acc, qs)?;
Ok(acc)
}
pub(crate) fn build_visage_count<V: DjogiVisage>(
qs: &VisageQuerySet<V>,
) -> Result<SqlAccumulator, crate::query::portable::PortablePredicateError> {
let mut acc = SqlAccumulator::new("");
acc.push_sql("SELECT COUNT(*) FROM ");
acc.push_sql(qs.table);
push_visage_where(&mut acc, qs)?;
Ok(acc)
}
pub(crate) fn build_visage_exists<V: DjogiVisage>(
qs: &VisageQuerySet<V>,
) -> Result<SqlAccumulator, crate::query::portable::PortablePredicateError> {
let mut acc = SqlAccumulator::new("");
acc.push_sql("SELECT EXISTS (SELECT 1 FROM ");
acc.push_sql(qs.table);
push_visage_where(&mut acc, qs)?;
acc.push_sql(" LIMIT 1)");
Ok(acc)
}
fn push_visage_where<V: DjogiVisage>(
acc: &mut SqlAccumulator,
qs: &VisageQuerySet<V>,
) -> Result<(), crate::query::portable::PortablePredicateError> {
if !q_is_vacuously_true(&qs.condition) {
acc.push_sql(" WHERE ");
emit_q::<V::Model>(acc, &qs.condition, SqlEmitContext::root())?;
}
Ok(())
}
fn push_visage_tail<V: DjogiVisage>(
acc: &mut SqlAccumulator,
qs: &VisageQuerySet<V>,
) -> Result<(), crate::query::portable::PortablePredicateError> {
push_visage_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, None);
}
}
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(())
}
impl<V: DjogiVisage> VisageQuerySet<V>
where
V: FromPgRow + Send + Unpin + 'static,
{
pub fn fetch_all<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Vec<V>, DjogiError>> + Send + 'ctx
where
V: 'ctx,
{
async move {
let (sql, binds) = run_all_sql(&self)?;
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
rows.iter().map(|r| V::from_pg_row(r)).collect()
}
}
pub fn fetch_one<'ctx>(
mut self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<V, DjogiError>> + Send + 'ctx
where
V: 'ctx,
{
async move {
let table = self.table;
self.limit = Some(2);
let (sql, binds) = run_all_sql(&self)?;
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
match rows.len() {
0 => Err(DjogiError::not_found(table)),
1 => {
let row = rows
.into_iter()
.next()
.expect("rows.len() == 1 was just matched");
V::from_pg_row(&row)
}
n => Err(DjogiError::multiple_objects(table, n)),
}
}
}
pub fn first<'ctx>(
mut self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<Option<V>, DjogiError>> + Send + 'ctx
where
V: 'ctx,
{
async move {
self.limit = Some(1);
let (sql, binds) = run_all_sql(&self)?;
let params = as_params(&binds);
let rows = ctx.query_all(&sql, ¶ms).await?;
match rows.into_iter().next() {
None => Ok(None),
Some(r) => V::from_pg_row(&r).map(Some),
}
}
}
}
impl<V: DjogiVisage> VisageQuerySet<V> {
pub fn count<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<i64, DjogiError>> + Send + 'ctx
where
V: 'ctx,
{
async move {
let (sql, binds) = build_visage_count(&self)
.map_err(DjogiError::from)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
try_get_scalar::<i64>(&row, 0)
}
}
pub fn exists<'ctx>(
self,
ctx: &'ctx mut DjogiContext,
) -> impl Future<Output = Result<bool, DjogiError>> + Send + 'ctx
where
V: 'ctx,
{
async move {
let (sql, binds) = build_visage_exists(&self)
.map_err(DjogiError::from)?
.into_parts();
let params = as_params(&binds);
let row = ctx.query_one(&sql, ¶ms).await?;
try_get_scalar::<bool>(&row, 0)
}
}
}
fn and_q_into_q<T: crate::model::Model, A: IntoQ<T>>(current: Q<T>, addition: A) -> Q<T> {
let addition = addition.into_q();
if q_is_vacuously_true(¤t) {
addition
} else if q_is_vacuously_true(&addition) {
current
} else {
current & addition
}
}
fn reject_subquery_modifiers(
entry_point: &'static str,
ordering: &[OrderExpr],
limit: Option<i64>,
offset: Option<i64>,
) -> Result<(), DjogiError> {
let offending = if !ordering.is_empty() {
Some("order_by")
} else if limit.is_some() {
Some("limit")
} else if offset.is_some() {
Some("offset")
} else {
None
};
if let Some(modifier) = offending {
return Err(DjogiError::invalid_subquery_modifier(
entry_point,
match modifier {
"order_by" => {
"order_by is not meaningful in a subquery; remove it before embedding"
}
"limit" => "limit is not meaningful in a subquery; remove it before embedding",
"offset" => "offset is not meaningful in a subquery; remove it before embedding",
other => unreachable!(
"reject_subquery_modifiers: unhandled modifier {other:?} — offending selector out of sync"
),
},
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{VisageColumn, VisageExists, VisageQuerySet};
use crate::model::Model;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::SqlEmitContext;
use crate::query::field::djogi_field_macro_support::__make_djogi_field;
#[allow(dead_code)]
struct Src {
id: i64,
name: String,
}
impl crate::model::__sealed::Sealed for Src {}
#[allow(clippy::manual_async_fn)]
impl Model for Src {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"srcs"
}
fn pk_value(&self) -> &i64 {
&self.id
}
fn descriptor() -> &'static crate::descriptor::ModelDescriptor {
unimplemented!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unimplemented!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unimplemented!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
{
async { unimplemented!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
async { unimplemented!() }
}
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 { unimplemented!() }
}
}
struct SrcView;
impl crate::visage_boundary::private::Sealed<Src> for SrcView {}
impl crate::visage_boundary::DjogiVisageOf<Src> for SrcView {}
impl crate::visage::private::Sealed for SrcView {}
impl crate::visage::DjogiVisage for SrcView {
type Model = Src;
const SCOPE: &'static str = "public";
const COLUMNS: &'static [&'static str] = &["id", "name"];
const PROJECTIONS: &'static [crate::__private::ProjectionEntry] = &[];
const PROJECTION_LIST: &'static str = "id, name";
}
fn test_col<U>(name: &'static str) -> VisageColumn<SrcView, U> {
VisageColumn::__new_for_visage_column(name, crate::__private::visage_column_seal::TOKEN)
}
fn emit_expr_sql(expr: crate::expr::Expr<bool>) -> String {
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &expr.node, SqlEmitContext::root()).expect("emit");
acc.sql().trim().to_string()
}
#[test]
fn selecting_builds_visage_subquery_emitting_select_col() {
let sub = VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.selecting(test_col::<i64>("id"))
.expect("clean queryset");
let node = sub.__into_subquery_node();
let mut acc = SqlAccumulator::new("");
crate::expr::sql::__emit_subquery_for_test(&mut acc, &node, SqlEmitContext::root())
.expect("emit");
assert_eq!(acc.sql().trim(), "SELECT id FROM srcs");
}
#[test]
fn selecting_rejects_order_by() {
let name_field =
__make_djogi_field::<Src, String>("name", |_r: &Src| -> &String { unreachable!() });
let err = VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.order_by(vec![name_field.asc()])
.selecting(test_col::<i64>("id"))
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("order_by"), "got: {err}");
}
#[test]
fn selecting_rejects_limit() {
let err = VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.limit(10)
.selecting(test_col::<i64>("id"))
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("limit"), "got: {err}");
}
#[test]
fn selecting_rejects_offset() {
let err = VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.offset(5)
.selecting(test_col::<i64>("id"))
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("offset"), "got: {err}");
}
#[test]
fn selecting_subquery_narrows_to_one_column_and_keeps_where() {
let predicate = crate::query::field::FieldRef::<Src, String>::new("name")
.as_expr()
.eq(crate::expr::Expr::literal("x".to_string()));
let sub = VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.filter(predicate)
.selecting(test_col::<i64>("id"))
.expect("clean queryset");
let node = sub.__into_subquery_node();
let mut acc = SqlAccumulator::new("");
crate::expr::sql::__emit_subquery_for_test(&mut acc, &node, SqlEmitContext::root())
.expect("emit");
let sql = acc.sql();
assert!(sql.contains("SELECT id FROM srcs WHERE"), "got: {sql}");
assert!(!sql.contains("id, name"), "got: {sql}");
assert!(sql.contains("name = $1"), "got: {sql}");
}
#[test]
fn visage_exists_emits_exists_subquery() {
let exists = VisageExists::new(VisageQuerySet::<SrcView>::new_for_visage(
"srcs", "id, name",
))
.expect("clean queryset");
assert_eq!(
emit_expr_sql(exists.as_expr()),
"EXISTS (SELECT 1 FROM srcs)"
);
}
#[test]
fn visage_exists_not_exists_wraps_in_not() {
let exists = VisageExists::new(VisageQuerySet::<SrcView>::new_for_visage(
"srcs", "id, name",
))
.expect("clean queryset");
assert_eq!(
emit_expr_sql(exists.not_exists()),
"NOT (EXISTS (SELECT 1 FROM srcs))"
);
}
#[test]
fn visage_exists_rejects_order_by() {
let name_field =
__make_djogi_field::<Src, String>("name", |_r: &Src| -> &String { unreachable!() });
let err = VisageExists::new(
VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name")
.order_by(vec![name_field.asc()]),
)
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("order_by"), "got: {err}");
}
#[test]
fn visage_exists_rejects_limit() {
let err = VisageExists::new(
VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name").limit(10),
)
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("limit"), "got: {err}");
}
#[test]
fn visage_exists_rejects_offset() {
let err = VisageExists::new(
VisageQuerySet::<SrcView>::new_for_visage("srcs", "id, name").offset(5),
)
.unwrap_err();
assert!(matches!(
err,
crate::DjogiError::InvalidSubqueryModifier { .. }
));
assert!(err.to_string().contains("offset"), "got: {err}");
}
}