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