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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
//! GROUP BY, HAVING, window functions, CTE, set operations, aggregates, and projection.
use std::marker::PhantomData;
use crate::entity::IEntityType;
use crate::error::EFResult;
use crate::provider::{DbValue, DbValueConvertError};
use super::super::ast::{BoolExpr, OrderDirection};
use super::super::compile::{collect_bool_expr_values, convert_aggregate_cell};
use super::super::cte::{CteSpec, SetOpSpec, SetOperator};
use super::super::having_expr::{AggKind, CompareOp, HavingExpr};
use super::super::select::SelectQueryBuilder;
use super::super::window::{WindowFuncKind, WindowSpec};
use super::core::QueryBuilder;
impl<T: IEntityType> QueryBuilder<T> {
/// Adds a GROUP BY clause.
///
/// `#[doc(hidden)]` — called by `linq!(group_by (b.cat, b.author))` expansion.
#[doc(hidden)]
pub fn group_by_internal(mut self, columns: &'static [&'static str]) -> Self {
self.state.group_bys = columns.iter().map(|s| s.to_string()).collect();
self
}
/// Adds a HAVING condition.
///
/// `#[doc(hidden)]` — called by `linq!(having count(b.id) > 1)` expansion.
/// Constructs `agg(column) op ?` with the value pushed to parameters.
#[doc(hidden)]
pub fn having_internal(
mut self,
agg: &str,
column: &str,
op: &str,
value: impl Into<DbValue>,
) -> Self {
let agg_kind = AggKind::from_name(agg)
.unwrap_or_else(|| panic!("invalid aggregate name in having_internal: {agg}"));
let cmp_op = CompareOp::from_symbol(op)
.unwrap_or_else(|| panic!("invalid operator in having_internal: {op}"));
let db_val = value.into();
self.state.parameters.push(db_val.clone());
self.state.havings.push(HavingExpr::Compare {
agg: agg_kind,
col: column.to_string(),
op: cmp_op,
value: db_val,
});
self
}
/// Adds a HAVING condition from a `HavingExpr` AST.
///
/// `#[doc(hidden)]` — called by `linq!(having <expr>)` expansion when the
/// having clause contains boolean combinations (`AND`/`OR`/`NOT`) or
/// aggregate-versus-aggregate comparisons. The expression is stored as an
/// AST node and compiled to SQL at `to_sql_with` time using the provider's
/// placeholder syntax; bound parameters are collected via
/// [`HavingExpr::collect_params`] and pushed to `state.parameters`.
#[doc(hidden)]
pub fn having_expr_internal(mut self, expr: HavingExpr) -> Self {
self.state.parameters.extend(expr.collect_params());
self.state.havings.push(expr);
self
}
// -------------------------------------------------------------------
// Window functions & CTE (v1.1)
// -------------------------------------------------------------------
/// Adds a window function projection to the SELECT list.
///
/// `#[doc(hidden)]` — called by `linq!(window ...)` expansion.
///
/// - `func`: window function name (e.g. `"row_number"`, `"sum"`, `"lag"`).
/// - `column`: the column argument (`None` for ranking functions).
/// - `partition_by`: PARTITION BY columns.
/// - `order_by`: ORDER BY columns as `(column, descending)` pairs.
/// - `alias`: the output column alias.
#[doc(hidden)]
pub fn window_internal(
mut self,
func: &str,
column: Option<&str>,
partition_by: &'static [&'static str],
order_by: &'static [(&'static str, bool)],
alias: &str,
) -> Self {
let kind = WindowFuncKind::from_name(func)
.unwrap_or_else(|| panic!("invalid window function name: {func}"));
if kind.takes_column() && column.is_none() {
panic!("window function `{func}` requires a column argument");
}
let spec = WindowSpec {
func: kind,
column: column.map(|s| s.to_string()),
partition_by: partition_by.iter().map(|s| s.to_string()).collect(),
order_by: order_by
.iter()
.map(|(c, d)| {
(
c.to_string(),
if *d {
OrderDirection::Descending
} else {
OrderDirection::Ascending
},
)
})
.collect(),
alias: alias.to_string(),
};
self.state.windows.push(spec);
self
}
/// Adds a CTE (Common Table Expression) definition to the query (raw mode).
///
/// `#[doc(hidden)]` — called by runtime API users.
///
/// The CTE body is a pre-compiled SQL string with `?` placeholders; its
/// parameter values are prepended to the query's parameter vector at
/// execution time so that placeholder ordering remains contiguous.
///
/// **Note**: Raw mode emits `?` placeholders verbatim and does not convert
/// them to provider-specific syntax (`$N` on PostgreSQL). For
/// provider-correct placeholders, use `with_cte_typed` (via
/// `linq!(with ...)`).
#[doc(hidden)]
pub fn with_cte_internal(
mut self,
name: &str,
sql: &str,
params: Vec<DbValue>,
columns: &'static [&'static str],
) -> Self {
let cte = CteSpec {
name: name.to_string(),
sql: sql.to_string(),
table: String::new(),
where_expr: None,
params,
columns: columns.iter().map(|s| s.to_string()).collect(),
is_recursive: false,
recursive_link: None,
};
self.state.ctes.push(cte);
self
}
/// Adds a typed CTE definition (typed mode), used by `linq!(with ...)`.
///
/// `#[doc(hidden)]` — called by `linq!(with name as |e: T| ...)` expansion.
///
/// The CTE body `SELECT * FROM <table> WHERE <where_expr>` is compiled at
/// `to_sql_with` time using the provider's placeholder syntax, ensuring
/// correct `$N` numbering on PostgreSQL and `?` on SQLite/MySQL.
///
/// Parameter values are extracted from `where_expr` via
/// `collect_bool_expr_values` and stored in `params` so that `all_params()`
/// returns them in the correct order (CTE params first).
#[doc(hidden)]
pub fn with_cte_typed(mut self, name: &str, table: &str, where_expr: BoolExpr) -> Self {
let params = collect_bool_expr_values(&where_expr);
let cte = CteSpec {
name: name.to_string(),
sql: String::new(),
table: table.to_string(),
where_expr: Some(where_expr),
params,
columns: Vec::new(),
is_recursive: false,
recursive_link: None,
};
self.state.ctes.push(cte);
self
}
/// Adds a typed recursive CTE definition, used by
/// `linq!(with recursive name as |e: T| ...; link e.fk to e.pk)`.
///
/// `#[doc(hidden)]` — called by `linq!(with recursive ...)` expansion.
///
/// Generates `WITH RECURSIVE name AS (anchor UNION ALL SELECT t.* FROM
/// <table> t JOIN name ON t.<link_fk> = name.<link_pk>)` where `anchor` is
/// `SELECT * FROM <table> WHERE <where_expr>` (or `SELECT * FROM <table>`
/// if `where_expr` is `BoolExpr::raw("")`).
#[doc(hidden)]
pub fn with_recursive_cte_typed(
mut self,
name: &str,
table: &str,
link_fk: &str,
link_pk: &str,
where_expr: BoolExpr,
) -> Self {
let params = collect_bool_expr_values(&where_expr);
let cte = CteSpec {
name: name.to_string(),
sql: String::new(),
table: table.to_string(),
where_expr: Some(where_expr),
params,
columns: Vec::new(),
is_recursive: true,
recursive_link: Some((link_fk.to_string(), link_pk.to_string())),
};
self.state.ctes.push(cte);
self
}
/// Changes the FROM clause to reference a CTE name (or any table/subquery).
///
/// Used in combination with `with_cte_internal` to query from a CTE:
/// ```ignore
/// builder.with_cte_internal("cte", "SELECT ...", params, &[])
/// .from_cte("cte")
/// ```
#[doc(hidden)]
pub fn from_cte(mut self, name: &str) -> Self {
self.state.from = name.to_string();
self
}
// -------------------------------------------------------------------
// Set operations (UNION / INTERSECT / EXCEPT)
// -------------------------------------------------------------------
/// Appends a UNION operand.
///
/// `operand` is a `(sql, params)` tuple from `QueryBuilder::compile_sql()`.
/// Per D5, the operand should not contain ORDER BY / LIMIT.
#[doc(hidden)]
pub fn union_internal(mut self, operand: (String, Vec<DbValue>)) -> Self {
self.state.set_operations.push(SetOpSpec {
operator: SetOperator::Union,
operand_sql: operand.0,
operand_params: operand.1,
});
self
}
/// Appends a UNION ALL operand.
#[doc(hidden)]
pub fn union_all_internal(mut self, operand: (String, Vec<DbValue>)) -> Self {
self.state.set_operations.push(SetOpSpec {
operator: SetOperator::UnionAll,
operand_sql: operand.0,
operand_params: operand.1,
});
self
}
/// Appends an INTERSECT operand.
#[doc(hidden)]
pub fn intersect_internal(mut self, operand: (String, Vec<DbValue>)) -> Self {
self.state.set_operations.push(SetOpSpec {
operator: SetOperator::Intersect,
operand_sql: operand.0,
operand_params: operand.1,
});
self
}
/// Appends an EXCEPT operand.
#[doc(hidden)]
pub fn except_internal(mut self, operand: (String, Vec<DbValue>)) -> Self {
self.state.set_operations.push(SetOpSpec {
operator: SetOperator::Except,
operand_sql: operand.0,
operand_params: operand.1,
});
self
}
// -------------------------------------------------------------------
// Aggregate terminal methods
// -------------------------------------------------------------------
/// Executes a SUM aggregation query.
///
/// `#[doc(hidden)]` — called by `linq!(sum b.views)` expansion.
#[doc(hidden)]
pub async fn sum_internal(self, column: &'static str) -> EFResult<f64> {
let mut state = self.state.clone();
state.aggregate = Some("SUM".to_string());
state.aggregate_column = Some(column.to_string());
let provider = self.provider.as_ref().ok_or_else(|| {
crate::error::EFError::configuration(
"No provider attached to QueryBuilder.".to_string(),
)
})?;
let sql = Self::compile_state_sql(&state, provider);
let params = state.all_params();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
if let Some(first) = rows.first().and_then(|r| r.first()) {
f64::try_from(first.clone()).map_err(|_| {
crate::error::EFError::type_conversion("SUM result is not f64".to_string())
})
} else {
Ok(0.0)
}
}
/// Executes an AVG aggregation query.
///
/// `#[doc(hidden)]` — called by `linq!(avg b.rating)` expansion.
#[doc(hidden)]
pub async fn avg_internal(self, column: &'static str) -> EFResult<f64> {
let mut state = self.state.clone();
state.aggregate = Some("AVG".to_string());
state.aggregate_column = Some(column.to_string());
let provider = self.provider.as_ref().ok_or_else(|| {
crate::error::EFError::configuration(
"No provider attached to QueryBuilder.".to_string(),
)
})?;
let sql = Self::compile_state_sql(&state, provider);
let params = state.all_params();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
if let Some(first) = rows.first().and_then(|r| r.first()) {
f64::try_from(first.clone()).map_err(|_| {
crate::error::EFError::type_conversion("AVG result is not f64".to_string())
})
} else {
Ok(0.0)
}
}
/// Executes a MIN aggregation query, returning the typed result.
///
/// `#[doc(hidden)]` — called by `linq!(min b.rating)` expansion. The target
/// type `V` is inferred from the call site (e.g. `let v: i64 = ...`).
#[doc(hidden)]
pub async fn min_internal<V>(self, column: &'static str) -> EFResult<Option<V>>
where
V: TryFrom<DbValue, Error = DbValueConvertError>,
{
let mut state = self.state.clone();
state.aggregate = Some("MIN".to_string());
state.aggregate_column = Some(column.to_string());
let provider = self.provider.as_ref().ok_or_else(|| {
crate::error::EFError::configuration(
"No provider attached to QueryBuilder.".to_string(),
)
})?;
let sql = Self::compile_state_sql(&state, provider);
let params = state.all_params();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
convert_aggregate_cell::<V>(rows)
}
/// Executes a MAX aggregation query, returning the typed result.
///
/// `#[doc(hidden)]` — called by `linq!(max b.rating)` expansion. The target
/// type `V` is inferred from the call site (e.g. `let v: i64 = ...`).
#[doc(hidden)]
pub async fn max_internal<V>(self, column: &'static str) -> EFResult<Option<V>>
where
V: TryFrom<DbValue, Error = DbValueConvertError>,
{
let mut state = self.state.clone();
state.aggregate = Some("MAX".to_string());
state.aggregate_column = Some(column.to_string());
let provider = self.provider.as_ref().ok_or_else(|| {
crate::error::EFError::configuration(
"No provider attached to QueryBuilder.".to_string(),
)
})?;
let sql = Self::compile_state_sql(&state, provider);
let params = state.all_params();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
convert_aggregate_cell::<V>(rows)
}
// -------------------------------------------------------------------
// Projection
// -------------------------------------------------------------------
/// Projects to named columns and returns raw row values.
///
/// `#[doc(hidden)]` — called by `linq!(select (b.id, b.title))` expansion.
#[doc(hidden)]
pub fn select_internal(self, columns: &'static [&'static str]) -> SelectQueryBuilder<T> {
let mut state = self.state.clone();
state.projected_columns = Some(columns.iter().map(|s| s.to_string()).collect());
SelectQueryBuilder {
state,
provider: self.provider,
_phantom: PhantomData,
}
}
}