mool 0.2.0

A source-first Rust database toolkit for typed SQL queries, migrations, and model metadata
Documentation
//! Window-function SQL rendering helpers.

use super::dialect::{self, DialectFeature};
use super::expr::{ExprNode, OrderExpr};
use super::render::{RenderMode, Renderer};
use super::window::{FrameBound, FrameBoundKind, FrameUnit, WindowFrame, WindowSpec};
use crate::QueryError;

pub(super) fn render_over(
    renderer: &mut Renderer,
    expr: &ExprNode,
    window: &WindowSpec,
    mode: RenderMode<'_>,
) -> Result<String, QueryError> {
    if matches!(mode, RenderMode::MutationRoot { .. }) {
        return Err(QueryError::BindError(
            "window functions are not allowed in mutation statements".to_string(),
        ));
    }
    validate_window_base(expr)?;
    dialect::validate_feature(renderer.dialect(), DialectFeature::WindowFunctions)?;
    let inner = renderer.render_expr(expr, mode)?;
    let spec = render_window(renderer, window, mode)?;
    Ok(format!("{inner} OVER ({spec})"))
}

fn validate_window_base(expr: &ExprNode) -> Result<(), QueryError> {
    match expr {
        ExprNode::Function { function, .. } if function.supports_window() => Ok(()),
        _ => Err(QueryError::BindError(
            "OVER requires a window-capable function".to_string(),
        )),
    }
}

fn render_window(
    renderer: &mut Renderer,
    window: &WindowSpec,
    mode: RenderMode<'_>,
) -> Result<String, QueryError> {
    let mut parts = Vec::new();
    push_partitions(renderer, window, mode, &mut parts)?;
    push_orders(renderer, window, mode, &mut parts)?;
    if let Some(frame) = &window.frame {
        parts.push(render_frame(renderer, frame, mode)?);
    }
    Ok(parts.join(" "))
}

fn push_partitions(
    renderer: &mut Renderer,
    window: &WindowSpec,
    mode: RenderMode<'_>,
    parts: &mut Vec<String>,
) -> Result<(), QueryError> {
    if window.partitions.is_empty() {
        return Ok(());
    }
    let rendered = window
        .partitions
        .iter()
        .map(|expr| renderer.render_expr(expr, mode))
        .collect::<Result<Vec<_>, _>>()?;
    parts.push(format!("PARTITION BY {}", rendered.join(", ")));
    Ok(())
}

fn push_orders(
    renderer: &mut Renderer,
    window: &WindowSpec,
    mode: RenderMode<'_>,
    parts: &mut Vec<String>,
) -> Result<(), QueryError> {
    if window.orders.is_empty() {
        return Ok(());
    }
    let mut rendered = Vec::with_capacity(window.orders.len());
    for order in &window.orders {
        rendered.push(render_order(renderer, order, mode)?);
    }
    parts.push(format!("ORDER BY {}", rendered.join(", ")));
    Ok(())
}

fn render_order(
    renderer: &mut Renderer,
    order: &OrderExpr,
    mode: RenderMode<'_>,
) -> Result<String, QueryError> {
    let direction = if order.desc { "DESC" } else { "ASC" };
    Ok(format!(
        "{} {direction}",
        renderer.render_expr(&order.expr, mode)?
    ))
}

fn render_frame(
    renderer: &mut Renderer,
    frame: &WindowFrame,
    mode: RenderMode<'_>,
) -> Result<String, QueryError> {
    let unit = match frame.unit {
        FrameUnit::Rows => "ROWS",
        FrameUnit::Range => "RANGE",
    };
    Ok(format!(
        "{unit} BETWEEN {} AND {}",
        render_bound(renderer, &frame.start, mode)?,
        render_bound(renderer, &frame.end, mode)?
    ))
}

fn render_bound(
    renderer: &mut Renderer,
    bound: &FrameBound,
    mode: RenderMode<'_>,
) -> Result<String, QueryError> {
    match bound.kind {
        FrameBoundKind::UnboundedPreceding => Ok("UNBOUNDED PRECEDING".to_string()),
        FrameBoundKind::CurrentRow => Ok("CURRENT ROW".to_string()),
        FrameBoundKind::UnboundedFollowing => Ok("UNBOUNDED FOLLOWING".to_string()),
        FrameBoundKind::Preceding => render_offset(renderer, bound, mode, "PRECEDING"),
        FrameBoundKind::Following => render_offset(renderer, bound, mode, "FOLLOWING"),
    }
}

fn render_offset(
    renderer: &mut Renderer,
    bound: &FrameBound,
    mode: RenderMode<'_>,
    suffix: &str,
) -> Result<String, QueryError> {
    let Some(expr) = bound.expr.as_ref() else {
        return Err(QueryError::BindError(format!(
            "{suffix} frame bound requires an offset"
        )));
    };
    Ok(format!("{} {suffix}", renderer.render_expr(expr, mode)?))
}