use crate::model::Model;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::order::OrderExpr;
use super::window::WindowSpec;
#[derive(Debug, Clone)]
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub struct QualifyCondition {
pub(crate) alias: &'static str,
pub(crate) op: QualifyOp,
pub(crate) value: QualifyValue,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum QualifyValue {
Int(i64),
Float(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QualifyOp {
Lt,
Lte,
Eq,
Gte,
Gt,
}
impl QualifyOp {
pub(crate) fn sql(self) -> &'static str {
match self {
QualifyOp::Lt => "<",
QualifyOp::Lte => "<=",
QualifyOp::Eq => "=",
QualifyOp::Gte => ">=",
QualifyOp::Gt => ">",
}
}
}
impl QualifyCondition {
pub(crate) fn push_outer_where(&self, acc: &mut SqlAccumulator) {
acc.push_sql(self.alias);
acc.push_sql(" ");
acc.push_sql(self.op.sql());
acc.push_sql(" ");
match self.value {
QualifyValue::Int(v) => acc.push_bind(v),
QualifyValue::Float(v) => acc.push_bind(v),
}
}
}
fn build_qualify(alias: Option<&'static str>, op: QualifyOp, value: i64) -> QualifyCondition {
QualifyCondition {
alias: alias.expect(
"qualify can only reference a window annotation that was registered with .alias(\"…\")",
),
op,
value: QualifyValue::Int(value),
}
}
fn build_qualify_f64(alias: Option<&'static str>, op: QualifyOp, value: f64) -> QualifyCondition {
QualifyCondition {
alias: alias.expect(
"qualify can only reference a window annotation that was registered with .alias(\"…\")",
),
op,
value: QualifyValue::Float(value),
}
}
mod sealed_ranking {
pub trait Sealed {}
}
pub trait WindowRanking: sealed_ranking::Sealed {
fn alias_name(&self) -> Option<&'static str>;
fn lt(&self, value: i64) -> QualifyCondition {
build_qualify(self.alias_name(), QualifyOp::Lt, value)
}
fn lte(&self, value: i64) -> QualifyCondition {
build_qualify(self.alias_name(), QualifyOp::Lte, value)
}
fn eq(&self, value: i64) -> QualifyCondition {
build_qualify(self.alias_name(), QualifyOp::Eq, value)
}
fn gte(&self, value: i64) -> QualifyCondition {
build_qualify(self.alias_name(), QualifyOp::Gte, value)
}
fn gt(&self, value: i64) -> QualifyCondition {
build_qualify(self.alias_name(), QualifyOp::Gt, value)
}
}
macro_rules! define_window_rank_fn {
($type_name:ident, $sql_name:literal, $example_name:literal) => {
#[doc = concat!(
"`",
$sql_name,
"() OVER (...)` window-only annotation returning `i64`.\n\n",
"# What\n\n",
"Use this type inside [`QuerySet::annotate`](crate::query::QuerySet::annotate) ",
"when each returned model row needs a per-partition ranking value. ",
"The function is window-only: Djogi always emits `OVER (...)`; it never ",
"lowers to a bare function call.\n\n",
"# Why\n\n",
"PostgreSQL 18 does not support a `QUALIFY` clause, so filters over ",
"window outputs are lowered by [`AnnotatedQuerySet::qualify`](crate::query::AnnotatedQuerySet::qualify) ",
"to an outer `WHERE` over a derived table. The alias supplied with ",
"[`alias`](Self::alias) becomes the column name that outer filter can reference.\n\n",
"# How\n\n",
"Build the window with [`partition_by`](Self::partition_by), ",
"[`order_by`](Self::order_by), and a required [`alias`](Self::alias). ",
"The alias must be a plain unquoted PostgreSQL identifier.\n\n",
"# Example\n\n",
"```ignore\n",
"use djogi::prelude::*;\n\n",
"let rows = Elephant::objects()\n",
" .annotate(|e| ", stringify!($type_name), "::new()\n",
" .partition_by(e.herd_id())\n",
" .order_by(e.score().desc())\n",
" .alias(\"", $example_name, "\"))\n",
" .qualify(|w| w.lte(3))\n",
" .fetch_all(&mut ctx)\n",
" .await?;\n",
"# Ok::<_, djogi::DjogiError>(())\n",
"```\n\n",
"The SQL shape is a derived table rather than `QUALIFY`:\n\n",
"```sql\n",
"SELECT * FROM (\n",
" SELECT t.id, ",
$sql_name,
"() OVER (PARTITION BY herd_id ORDER BY score DESC) AS ",
$example_name,
"\n",
" FROM elephants AS t\n",
") AS __djogi_q\n",
"WHERE ",
$example_name,
" <= $1\n",
"```"
)]
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone, Default)]
pub struct $type_name {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
}
impl $type_name {
#[doc = concat!(
"Construct an empty `",
$sql_name,
"() OVER ()` window annotation.\n\n",
"# What\n\n",
"The returned builder has no partitioning, ordering, frame, or alias yet.\n\n",
"# Why\n\n",
"The window spec starts empty so callers can opt into the exact ",
"partition and order required by the ranking query. The function still ",
"remains window-only because emission appends `OVER ()` even when no ",
"builder methods are called.\n\n",
"# How\n\n",
"Chain [`partition_by`](Self::partition_by), [`order_by`](Self::order_by), ",
"and the required [`alias`](Self::alias) before passing it to `.annotate(...)`.\n\n",
"# Example\n\n",
"```ignore\n",
"let rank = ", stringify!($type_name), "::new()\n",
" .order_by(fields.score().desc())\n",
" .alias(\"", $example_name, "\");\n",
"```"
)]
pub fn new() -> Self {
Self {
window: WindowSpec::default(),
alias: None,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
self.window.partition_by.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql($sql_name);
acc.push_sql("()");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
pub fn lt(&self, value: i64) -> QualifyCondition {
<Self as WindowRanking>::lt(self, value)
}
pub fn lte(&self, value: i64) -> QualifyCondition {
<Self as WindowRanking>::lte(self, value)
}
pub fn eq(&self, value: i64) -> QualifyCondition {
<Self as WindowRanking>::eq(self, value)
}
pub fn gte(&self, value: i64) -> QualifyCondition {
<Self as WindowRanking>::gte(self, value)
}
pub fn gt(&self, value: i64) -> QualifyCondition {
<Self as WindowRanking>::gt(self, value)
}
}
impl sealed_ranking::Sealed for $type_name {}
impl WindowRanking for $type_name {
fn alias_name(&self) -> Option<&'static str> {
self.alias
}
}
};
}
define_window_rank_fn!(RowNumber, "ROW_NUMBER", "rank");
define_window_rank_fn!(Rank, "RANK", "rank");
define_window_rank_fn!(DenseRank, "DENSE_RANK", "dense_rank");
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone, Default)]
pub struct PercentRankWindow {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
}
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone, Default)]
pub struct CumeDistWindow {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
}
macro_rules! impl_zero_arg_f64_window {
($type_name:ident, $sql_name:literal) => {
impl $type_name {
pub fn new() -> Self {
Self {
window: WindowSpec::default(),
alias: None,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
self.window
.partition_by
.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql($sql_name);
acc.push_sql("()");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub fn lt(&self, value: f64) -> QualifyCondition {
build_qualify_f64(self.alias_name(), QualifyOp::Lt, value)
}
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub fn lte(&self, value: f64) -> QualifyCondition {
build_qualify_f64(self.alias_name(), QualifyOp::Lte, value)
}
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub fn eq(&self, value: f64) -> QualifyCondition {
build_qualify_f64(self.alias_name(), QualifyOp::Eq, value)
}
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub fn gte(&self, value: f64) -> QualifyCondition {
build_qualify_f64(self.alias_name(), QualifyOp::Gte, value)
}
#[must_use = "qualify conditions are only meaningful once handed to .qualify(...)"]
pub fn gt(&self, value: f64) -> QualifyCondition {
build_qualify_f64(self.alias_name(), QualifyOp::Gt, value)
}
}
};
}
impl_zero_arg_f64_window!(PercentRankWindow, "PERCENT_RANK");
impl_zero_arg_f64_window!(CumeDistWindow, "CUME_DIST");
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone)]
pub struct NtileWindow {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
pub(crate) buckets: i32,
}
impl NtileWindow {
pub fn new(buckets: i32) -> Self {
Self {
window: WindowSpec::default(),
alias: None,
buckets,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
self.window
.partition_by
.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql("NTILE(");
acc.push_bind(self.buckets as i64);
acc.push_sql(")");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
}
macro_rules! define_column_arg_window_fn {
($type_name:ident, $sql_name:literal) => {
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone)]
pub struct $type_name<V> {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
pub(crate) target_column: &'static str,
pub(crate) _out: std::marker::PhantomData<fn() -> V>,
}
impl<V> $type_name<V> {
pub fn new<M, S>(target: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
Self {
window: WindowSpec::default(),
alias: None,
target_column: target.into_sql_field().column(),
_out: std::marker::PhantomData,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V2, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V2>,
{
self.window
.partition_by
.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql($sql_name);
acc.push_sql("(");
acc.push_sql(self.target_column);
acc.push_sql(")");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
}
};
}
define_column_arg_window_fn!(FirstValueWindow, "FIRST_VALUE");
define_column_arg_window_fn!(LastValueWindow, "LAST_VALUE");
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone)]
pub struct LeadWindow<V> {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
pub(crate) target_column: &'static str,
pub(crate) offset: Option<i64>,
pub(crate) _out: std::marker::PhantomData<fn() -> V>,
}
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone)]
pub struct LagWindow<V> {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
pub(crate) target_column: &'static str,
pub(crate) offset: Option<i64>,
pub(crate) _out: std::marker::PhantomData<fn() -> V>,
}
macro_rules! impl_lead_lag {
($type_name:ident, $sql_name:literal) => {
impl<V> $type_name<V> {
pub fn new<M, S>(target: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
Self {
window: WindowSpec::default(),
alias: None,
target_column: target.into_sql_field().column(),
offset: None,
_out: std::marker::PhantomData,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn offset(mut self, n: i64) -> Self {
self.offset = Some(n);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V2, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V2>,
{
self.window
.partition_by
.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql($sql_name);
acc.push_sql("(");
acc.push_sql(self.target_column);
if let Some(off) = self.offset {
acc.push_sql(", ");
acc.push_bind(off);
}
acc.push_sql(")");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
}
};
}
impl_lead_lag!(LeadWindow, "LEAD");
impl_lead_lag!(LagWindow, "LAG");
#[must_use = "window functions are lazy annotations - dropping one omits the column"]
#[derive(Debug, Clone)]
pub struct NthValueWindow<V> {
pub(crate) window: WindowSpec,
pub(crate) alias: Option<&'static str>,
pub(crate) target_column: &'static str,
pub(crate) n: i64,
pub(crate) _out: std::marker::PhantomData<fn() -> V>,
}
impl<V> NthValueWindow<V> {
pub fn new<M, S>(target: S, n: i64) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V>,
{
Self {
window: WindowSpec::default(),
alias: None,
target_column: target.into_sql_field().column(),
n,
_out: std::marker::PhantomData,
}
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn partition_by<M, V2, S>(mut self, field: S) -> Self
where
M: Model,
S: crate::query::field::IntoSqlField<M, V2>,
{
self.window
.partition_by
.push(field.into_sql_field().column());
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn order_by(mut self, order: OrderExpr) -> Self {
push_order_expr(&mut self.window, order);
self
}
#[must_use = "window functions are immutable builders - use the returned value"]
pub fn alias(mut self, alias: &'static str) -> Self {
crate::ident::assert_user_supplied_ident(alias, "window_alias");
self.alias = Some(alias);
self
}
pub(crate) fn alias_name(&self) -> Option<&'static str> {
self.alias
}
pub(crate) fn push_annotated_column(&self, acc: &mut SqlAccumulator) {
acc.push_sql("NTH_VALUE(");
acc.push_sql(self.target_column);
acc.push_sql(", ");
acc.push_bind(self.n);
acc.push_sql(")");
self.window.emit(acc);
acc.push_sql(" AS ");
acc.push_sql(
self.alias
.expect("window function annotations are checked before SQL emission"),
);
}
}
fn push_order_expr(window: &mut WindowSpec, order: OrderExpr) {
match order {
OrderExpr::Column {
column, direction, ..
} => {
window.order_by.push((column, direction));
}
#[cfg(feature = "spatial")]
OrderExpr::SpatialDistance { .. } => {
panic!("window ranking order_by accepts field asc()/desc() orderings only")
}
}
}