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}