Skip to main content

prax_query/
projection.rs

1//! Scalar-subquery projections for SELECT clauses.
2//!
3//! Used by relation-aggregate virtual fields (`@count`, `@sum`, …) and
4//! by the `select: { _count: { rel: true } }` ad-hoc accessor (phase
5//! 5.5). The `sql` field uses the same `{N}` placeholder convention as
6//! [`crate::filter::Filter::ScalarSubquery`] — `{N}` resolves to the
7//! dialect placeholder for `params[N]`. Placeholders are renumbered
8//! into a single positional sequence at SqlBuilder time, so they
9//! compose cleanly with WHERE filters and other projections.
10
11use std::borrow::Cow;
12
13use crate::filter::FilterValue;
14
15/// A scalar-subquery projection added to a SELECT clause, emitted as
16/// `(<sql>) AS <alias>`. The alias is a codegen-controlled
17/// `&'static str` — never user input — so it can be safely interpolated
18/// into SQL after identifier quoting.
19#[derive(Debug, Clone)]
20pub struct ScalarProjection {
21    /// SQL fragment with `{N}` placeholders.
22    pub sql: Cow<'static, str>,
23    /// Parameter values referenced by the `{N}` placeholders.
24    pub params: Vec<FilterValue>,
25    /// Output column alias.
26    pub alias: &'static str,
27}
28
29impl ScalarProjection {
30    pub fn new(
31        sql: impl Into<Cow<'static, str>>,
32        params: Vec<FilterValue>,
33        alias: &'static str,
34    ) -> Self {
35        Self {
36            sql: sql.into(),
37            params,
38            alias,
39        }
40    }
41
42    /// Rewrite `{N}` placeholders to dialect-specific positional form,
43    /// offsetting by `offset` (the count of params already emitted by
44    /// earlier clauses in the same query).
45    ///
46    /// All `params` are appended to `out_params` in index order so the
47    /// caller's global param list stays consistent.
48    pub(crate) fn to_sql(
49        &self,
50        offset: usize,
51        dialect: &dyn crate::dialect::SqlDialect,
52        out_params: &mut Vec<FilterValue>,
53    ) -> String {
54        // Push this projection's params in order; {N} maps to global slot
55        // (offset + N + 1) — matching the Filter::ScalarSubquery convention.
56        for v in self.params.iter() {
57            out_params.push(v.clone());
58        }
59
60        let sql = &self.sql;
61        let mut out = String::with_capacity(sql.len() + self.params.len() * 4);
62        let mut chars = sql.chars().peekable();
63        while let Some(ch) = chars.next() {
64            if ch == '{' {
65                let mut digits = String::new();
66                while let Some(&c) = chars.peek() {
67                    if c == '}' {
68                        chars.next();
69                        break;
70                    }
71                    digits.push(c);
72                    chars.next();
73                }
74                let n: usize = digits.parse().unwrap_or_else(|_| {
75                    panic!(
76                        "ScalarProjection: invalid placeholder index `{{{}}}`",
77                        digits
78                    )
79                });
80                if n >= self.params.len() {
81                    panic!(
82                        "ScalarProjection: placeholder {{{}}} out of range (have {} params)",
83                        n,
84                        self.params.len()
85                    );
86                }
87                out.push_str(&dialect.placeholder(offset + n + 1));
88            } else {
89                out.push(ch);
90            }
91        }
92        out
93    }
94}