mod common;
use common::{MySqlLikeGenerator, PgLikeGenerator, SqliteLikeGenerator};
use rust_ef::provider::DbValue;
use rust_ef::query::{AggKind, BoolExpr, CompareOp, FilterCondition, HavingExpr, QueryState};
#[test]
fn test_pg_having_uses_dollar_n_placeholder() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
});
state.parameters.push(DbValue::I32(1));
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("HAVING COUNT(blog_id) > $1"),
"PG HAVING should use $1 placeholder, got: {sql}"
);
assert!(
!sql.contains("?"),
"PG HAVING must not contain `?` placeholder, got: {sql}"
);
}
#[test]
fn test_sqlite_having_uses_question_mark_placeholder() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
});
state.parameters.push(DbValue::I32(1));
let sql = state.to_sql_with(&SqliteLikeGenerator);
assert!(
sql.contains("HAVING COUNT(blog_id) > ?"),
"SQLite HAVING should use ? placeholder, got: {sql}"
);
}
#[test]
fn test_mysql_having_uses_question_mark_placeholder() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
});
state.parameters.push(DbValue::I32(1));
let sql = state.to_sql_with(&MySqlLikeGenerator);
assert!(
sql.contains("HAVING COUNT(blog_id) > ?"),
"MySQL HAVING should use ? placeholder, got: {sql}"
);
}
#[test]
fn test_pg_having_index_continues_from_where() {
let mut state = QueryState::new("blogs");
state.parameters.push(DbValue::String("tech".to_string()));
state.parameters.push(DbValue::I32(1));
state.where_expr = Some(BoolExpr::Filter(FilterCondition::new("category", "=", 1)));
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
});
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("WHERE category = $1"),
"WHERE should use $1, got: {sql}"
);
assert!(
sql.contains("HAVING COUNT(blog_id) > $2"),
"HAVING should continue at $2 after WHERE $1, got: {sql}"
);
}
#[test]
fn test_pg_having_index_continues_through_and_combination() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
let expr = HavingExpr::And(
Box::new(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
}),
Box::new(HavingExpr::Compare {
agg: AggKind::Sum,
col: "views".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(100),
}),
);
state.parameters.extend(expr.collect_params());
state.havings.push(expr);
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("HAVING (COUNT(blog_id) > $1 AND SUM(views) > $2)"),
"HAVING AND should use $1 and $2, got: {sql}"
);
}
#[test]
fn test_pg_having_index_continues_through_or_with_where() {
let mut state = QueryState::new("blogs");
state.parameters.push(DbValue::String("tech".to_string()));
state.where_expr = Some(BoolExpr::Filter(FilterCondition::new("category", "=", 1)));
state.group_bys = vec!["category".to_string()];
let having = HavingExpr::Or(
Box::new(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(5),
}),
Box::new(HavingExpr::Compare {
agg: AggKind::Sum,
col: "views".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(100),
}),
);
state.parameters.extend(having.collect_params());
state.havings.push(having);
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("WHERE category = $1"),
"WHERE should use $1, got: {sql}"
);
assert!(
sql.contains("HAVING (COUNT(blog_id) > $2 OR SUM(views) > $3)"),
"HAVING OR should continue at $2 and $3, got: {sql}"
);
}
#[test]
fn test_pg_having_not_preserves_index() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
let having = HavingExpr::Not(Box::new(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
}));
state.parameters.extend(having.collect_params());
state.havings.push(having);
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("HAVING NOT (COUNT(blog_id) > $1)"),
"HAVING NOT should use $1, got: {sql}"
);
}
#[test]
fn test_pg_having_compare_agg_emits_no_placeholder() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::CompareAgg {
left_agg: AggKind::Sum,
left_col: "views".to_string(),
op: CompareOp::Gt,
right_agg: AggKind::Count,
right_col: "blog_id".to_string(),
});
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("HAVING SUM(views) > COUNT(blog_id)"),
"HAVING CompareAgg should emit no placeholder, got: {sql}"
);
assert!(
!sql.contains('$'),
"CompareAgg must not emit any $N placeholder, got: {sql}"
);
}
#[test]
fn test_pg_having_multiple_clauses_join_with_and() {
let mut state = QueryState::new("blogs");
state.group_bys = vec!["category".to_string()];
state.havings.push(HavingExpr::Compare {
agg: AggKind::Count,
col: "blog_id".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
});
state.havings.push(HavingExpr::Compare {
agg: AggKind::Sum,
col: "views".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(100),
});
state.parameters.push(DbValue::I32(1));
state.parameters.push(DbValue::I32(100));
let sql = state.to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("HAVING COUNT(blog_id) > $1 AND SUM(views) > $2"),
"Multiple HAVING clauses should join with AND and continue indices, got: {sql}"
);
}
#[test]
fn test_collect_params_matches_placeholder_order() {
let expr = HavingExpr::And(
Box::new(HavingExpr::Compare {
agg: AggKind::Count,
col: "a".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(1),
}),
Box::new(HavingExpr::Or(
Box::new(HavingExpr::Compare {
agg: AggKind::Sum,
col: "b".to_string(),
op: CompareOp::Gt,
value: DbValue::I32(2),
}),
Box::new(HavingExpr::Not(Box::new(HavingExpr::Compare {
agg: AggKind::Avg,
col: "c".to_string(),
op: CompareOp::Lt,
value: DbValue::I32(3),
}))),
)),
);
let params = expr.collect_params();
assert_eq!(
params,
vec![DbValue::I32(1), DbValue::I32(2), DbValue::I32(3),],
"collect_params must return values in left-to-right traversal order"
);
let mut idx = 1usize;
let sql = expr.to_sql(&PgLikeGenerator, &mut idx);
assert!(
sql.contains("$1") && sql.contains("$2") && sql.contains("$3"),
"to_sql should emit $1, $2, $3 in order, got: {sql}"
);
assert_eq!(idx, 4, "param_idx should advance past all 3 params");
}