|
5 | 5 | //! the shared `resolve_root` produces the positional, resolved canonical |
6 | 6 | //! tree (the same resolver the PromQL path uses). |
7 | 7 |
|
8 | | -use asap_frontend_sql::{lower_sql, SqlCatalog}; |
| 8 | +use asap_frontend_sql::{lower_sql, lower_sql_dialect, SqlCatalog}; |
9 | 9 | use asap_types::pre_asap::schema::{Column, DataType, Schema}; |
10 | 10 | use asap_types::pre_asap::{ |
11 | 11 | AggIntent, CompareOpKind, GroupKeys, JoinKind, QueryExpr, Reduction, ScalarValue, Source, |
12 | 12 | WindowFuncKind, |
13 | 13 | }; |
14 | 14 | use asap_types::types::AccuracyTarget; |
| 15 | +use asap_types::workload::SqlDialect; |
15 | 16 |
|
16 | 17 | fn col(name: &str, dtype: DataType) -> Column { |
17 | 18 | Column::new(name, dtype, false) |
@@ -1392,3 +1393,76 @@ async fn array_agg_is_deliberately_rejected() { |
1392 | 1393 | "expected a clean UnsupportedAggregate, got {err}" |
1393 | 1394 | ); |
1394 | 1395 | } |
| 1396 | + |
| 1397 | +// ── Issue #225: catalog-driven ClickHouse builtins (countIf, generalizing |
| 1398 | +// uniqExact from #221) ─────────────────────────────────────────────────── |
| 1399 | + |
| 1400 | +async fn lower_clickhouse(sql: &str) -> QueryExpr { |
| 1401 | + lower_sql_dialect( |
| 1402 | + sql, |
| 1403 | + &catalog(), |
| 1404 | + SqlDialect::ClickhouseSQL, |
| 1405 | + AccuracyTarget::Exact, |
| 1406 | + ) |
| 1407 | + .await |
| 1408 | + .unwrap_or_else(|e| panic!("lower failed for {sql:?}: {e}")) |
| 1409 | +} |
| 1410 | + |
| 1411 | +#[tokio::test] |
| 1412 | +async fn count_if_lowers_to_a_sum_over_a_derived_indicator_column() { |
| 1413 | + // ClickHouse's `countIf(cond)` has no DataFusion equivalent at all, so it |
| 1414 | + // goes through the same stub-UDAF + catalog-driven `FunctionRewrite` |
| 1415 | + // mechanism `uniqExact` (#221) does — rewritten, before `lower_agg_intent` |
| 1416 | + // ever runs, to `sum(CASE WHEN cond THEN 1 ELSE 0 END)`. Not a plain |
| 1417 | + // `count(...) FILTER (WHERE cond)`: `AggIntent::Count` never consults its |
| 1418 | + // argument (always a row count), so the filter would be silently dropped; |
| 1419 | + // summing a 0/1 indicator keeps `cond` observable through the ordinary |
| 1420 | + // `Sum` path instead. |
| 1421 | + let qe = lower_clickhouse("SELECT countIf(bytes > 100) AS big FROM metrics").await; |
| 1422 | + let (by, measures) = find_aggregate(&qe).expect("expected an Aggregate"); |
| 1423 | + assert!(by.is_empty()); |
| 1424 | + assert!( |
| 1425 | + matches!(measures.as_slice(), [AggIntent::Sum { col: Some(_) }]), |
| 1426 | + "expected a Sum bound to the derived indicator column, got {measures:?}" |
| 1427 | + ); |
| 1428 | + let (_, materialized) = reducer_input_names(&qe); |
| 1429 | + assert!( |
| 1430 | + materialized, |
| 1431 | + "the indicator expression must be materialized in a Project beneath the Aggregate" |
| 1432 | + ); |
| 1433 | +} |
| 1434 | + |
| 1435 | +#[tokio::test] |
| 1436 | +async fn two_count_ifs_with_different_conditions_stay_distinct_reducers() { |
| 1437 | + // The corpus pattern (`countIf(operation = 'A'), countIf(operation = 'W')` |
| 1438 | + // in one GROUP BY) needs each call's own condition to survive as its own |
| 1439 | + // derived column, not collapse onto a shared one. |
| 1440 | + let qe = lower_clickhouse( |
| 1441 | + "SELECT service, countIf(bytes > 100) AS big, countIf(bytes <= 100) AS small \ |
| 1442 | + FROM metrics GROUP BY service", |
| 1443 | + ) |
| 1444 | + .await; |
| 1445 | + let (by, measures) = find_aggregate(&qe).expect("expected an Aggregate"); |
| 1446 | + assert_eq!(*by, GroupKeys::by(vec![0])); |
| 1447 | + assert!( |
| 1448 | + matches!( |
| 1449 | + measures.as_slice(), |
| 1450 | + [ |
| 1451 | + AggIntent::Sum { col: Some(a) }, |
| 1452 | + AggIntent::Sum { col: Some(b) } |
| 1453 | + ] if a != b |
| 1454 | + ), |
| 1455 | + "expected two distinct Sum reducers, got {measures:?}" |
| 1456 | + ); |
| 1457 | +} |
| 1458 | + |
| 1459 | +#[tokio::test] |
| 1460 | +async fn count_if_composes_with_group_by() { |
| 1461 | + let qe = lower_clickhouse( |
| 1462 | + "SELECT service, countIf(bytes > 100) AS big FROM metrics GROUP BY service", |
| 1463 | + ) |
| 1464 | + .await; |
| 1465 | + let (by, measures) = find_aggregate(&qe).expect("expected an Aggregate"); |
| 1466 | + assert_eq!(*by, GroupKeys::by(vec![0])); |
| 1467 | + assert!(matches!(measures.as_slice(), [AggIntent::Sum { .. }])); |
| 1468 | +} |
0 commit comments