#![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>,
}
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
}
#[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
}
}