use vibesql_ast::{Expression, OrderByItem, OrderDirection, SelectItem, SelectStmt};
pub fn is_simple_point_query(stmt: &SelectStmt) -> bool {
if stmt.with_clause.is_some() {
return false;
}
if stmt.set_operation.is_some() {
return false;
}
if stmt.into_table.is_some() || stmt.into_variables.is_some() {
return false;
}
if stmt.group_by.is_some() || stmt.having.is_some() || stmt.distinct {
return false;
}
let Some(from) = &stmt.from else {
return false;
};
if !matches!(from, vibesql_ast::FromClause::Table { .. }) {
return false;
}
if !has_simple_select_list(&stmt.select_list) {
return false;
}
if let Some(where_clause) = &stmt.where_clause {
if !is_simple_where_clause(where_clause) {
return false;
}
}
if let Some(order_by) = &stmt.order_by {
if !is_simple_order_by(order_by) {
return false;
}
if uses_select_alias(order_by, &stmt.select_list) {
return false;
}
}
true
}
pub fn is_streaming_aggregate_query(stmt: &SelectStmt) -> bool {
if stmt.with_clause.is_some() {
return false;
}
if stmt.set_operation.is_some() {
return false;
}
if stmt.group_by.is_some() || stmt.having.is_some() || stmt.distinct {
return false;
}
if stmt.order_by.is_some() {
return false;
}
let Some(from) = &stmt.from else {
return false;
};
if !matches!(from, vibesql_ast::FromClause::Table { .. }) {
return false;
}
if stmt.where_clause.is_none() {
return false;
}
if !has_simple_aggregate_select_list(&stmt.select_list) {
return false;
}
true
}
pub(crate) fn has_simple_aggregate_select_list(select_list: &[SelectItem]) -> bool {
if select_list.is_empty() {
return false;
}
for item in select_list {
match item {
SelectItem::Expression { expr, .. } => {
if !is_simple_aggregate_expression(expr) {
return false;
}
}
_ => return false,
}
}
true
}
pub(crate) fn is_simple_aggregate_expression(expr: &Expression) -> bool {
match expr {
Expression::AggregateFunction { name, args, distinct, .. } => {
if *distinct {
return false;
}
let upper_name = name.to_uppercase();
if !matches!(upper_name.as_str(), "SUM" | "COUNT" | "AVG" | "MIN" | "MAX") {
return false;
}
if args.len() != 1 {
return false;
}
matches!(&args[0], Expression::ColumnRef(_))
}
_ => false,
}
}
pub(crate) fn extract_simple_aggregate(
expr: &Expression,
table_schema: &vibesql_catalog::TableSchema,
) -> Option<(String, usize)> {
match expr {
Expression::AggregateFunction { name, args, distinct, .. } => {
if *distinct {
return None;
}
let upper_name = name.to_uppercase();
if !matches!(upper_name.as_str(), "SUM" | "COUNT" | "AVG" | "MIN" | "MAX") {
return None;
}
if args.len() != 1 {
return None;
}
if let Expression::ColumnRef(col_id) = &args[0] {
let column = col_id.column_canonical();
let col_idx = table_schema
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))?;
return Some((upper_name, col_idx));
}
None
}
_ => None,
}
}
pub(crate) fn is_simple_order_by(order_by: &[OrderByItem]) -> bool {
for item in order_by {
if !matches!(item.expr, Expression::ColumnRef(_)) {
return false;
}
}
true
}
pub(crate) fn uses_select_alias(order_by: &[OrderByItem], select_list: &[SelectItem]) -> bool {
let aliases: Vec<&str> = select_list
.iter()
.filter_map(|item| {
if let SelectItem::Expression { alias: Some(alias), .. } = item {
Some(alias.as_str())
} else {
None
}
})
.collect();
if aliases.is_empty() {
return false;
}
for item in order_by {
if let Expression::ColumnRef(col_id) = &item.expr {
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() {
let column = col_id.column_canonical();
if aliases.iter().any(|alias| alias.eq_ignore_ascii_case(column)) {
return true;
}
}
}
}
false
}
pub fn needs_sorting(
order_by: &[OrderByItem],
sorted_by: &Option<Vec<(String, OrderDirection)>>,
) -> bool {
let Some(sorted_cols) = sorted_by else {
return true; };
if order_by.len() > sorted_cols.len() {
return true; }
for (order_item, (col_name, col_dir)) in order_by.iter().zip(sorted_cols.iter()) {
let order_col = match &order_item.expr {
Expression::ColumnRef(col_id) => col_id.column_canonical(),
_ => return true, };
if !order_col.eq_ignore_ascii_case(col_name) {
return true;
}
if &order_item.direction != col_dir {
return true;
}
}
false }
pub(crate) fn has_simple_select_list(select_list: &[SelectItem]) -> bool {
for item in select_list {
match item {
SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => continue,
SelectItem::Expression { expr, .. } => {
if !is_simple_expression(expr) {
return false;
}
}
}
}
true
}
pub(crate) fn is_simple_expression(expr: &Expression) -> bool {
match expr {
Expression::ColumnRef(_) | Expression::Literal(_) => true,
Expression::BinaryOp { left, right, op } => {
matches!(
op,
vibesql_ast::BinaryOperator::Plus
| vibesql_ast::BinaryOperator::Minus
| vibesql_ast::BinaryOperator::Multiply
| vibesql_ast::BinaryOperator::Divide
| vibesql_ast::BinaryOperator::Concat
) && is_simple_expression(left)
&& is_simple_expression(right)
}
Expression::UnaryOp { expr, .. } => is_simple_expression(expr),
Expression::Cast { expr, .. } => is_simple_expression(expr),
_ => false,
}
}
pub(crate) fn is_simple_where_clause(expr: &Expression) -> bool {
match expr {
Expression::BinaryOp { left, op, right } => {
match op {
vibesql_ast::BinaryOperator::Equal
| vibesql_ast::BinaryOperator::NotEqual
| vibesql_ast::BinaryOperator::GreaterThan
| vibesql_ast::BinaryOperator::GreaterThanOrEqual
| vibesql_ast::BinaryOperator::LessThan
| vibesql_ast::BinaryOperator::LessThanOrEqual => {
is_column_or_literal(left) && is_column_or_literal(right)
}
vibesql_ast::BinaryOperator::And => {
is_simple_where_clause(left) && is_simple_where_clause(right)
}
vibesql_ast::BinaryOperator::Or => false,
_ => false,
}
}
Expression::Between { expr, low, high, .. } => {
is_column_or_literal(expr) && is_column_or_literal(low) && is_column_or_literal(high)
}
Expression::InList { expr, values, .. } => {
is_column_or_literal(expr) && values.iter().all(is_column_or_literal)
}
Expression::IsNull { expr, .. } => is_column_or_literal(expr),
Expression::Like { expr, pattern, .. } => {
is_column_or_literal(expr) && is_column_or_literal(pattern)
}
_ => false,
}
}
pub(crate) fn is_column_or_literal(expr: &Expression) -> bool {
matches!(expr, Expression::ColumnRef(_) | Expression::Literal(_))
}
#[cfg(test)]
mod tests {
use vibesql_ast::Statement;
use vibesql_parser::Parser;
use super::*;
fn parse_select(sql: &str) -> SelectStmt {
match Parser::parse_sql(sql).unwrap() {
Statement::Select(stmt) => *stmt,
_ => panic!("Expected SELECT statement"),
}
}
#[test]
fn test_simple_point_query_detection() {
assert!(is_simple_point_query(&parse_select("SELECT w_tax FROM warehouse WHERE w_id = 1")));
assert!(is_simple_point_query(&parse_select("SELECT * FROM users WHERE id = 123")));
assert!(is_simple_point_query(&parse_select("SELECT a, b FROM t WHERE x = 1 AND y = 2")));
assert!(is_simple_point_query(&parse_select("SELECT a FROM t WHERE x > 10")));
assert!(is_simple_point_query(&parse_select("SELECT a FROM t WHERE x BETWEEN 1 AND 10")));
assert!(is_simple_point_query(&parse_select("SELECT a FROM t WHERE x IN (1, 2, 3)")));
assert!(is_simple_point_query(&parse_select("SELECT a FROM t WHERE x IS NULL")));
}
#[test]
fn test_non_simple_query_detection() {
assert!(!is_simple_point_query(&parse_select("SELECT COUNT(*) FROM t WHERE id = 1")));
assert!(!is_simple_point_query(&parse_select("SELECT a FROM t1, t2 WHERE t1.id = t2.id")));
assert!(!is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE id IN (SELECT id FROM t2)"
)));
assert!(!is_simple_point_query(&parse_select("SELECT DISTINCT a FROM t")));
assert!(!is_simple_point_query(&parse_select("SELECT a FROM t GROUP BY a")));
assert!(!is_simple_point_query(&parse_select("WITH cte AS (SELECT 1) SELECT * FROM cte")));
assert!(!is_simple_point_query(&parse_select("SELECT a FROM t UNION SELECT b FROM t2")));
}
#[test]
fn test_or_not_simple() {
assert!(!is_simple_point_query(&parse_select("SELECT a FROM t WHERE x = 1 OR y = 2")));
}
#[test]
fn test_order_by_simple_queries() {
assert!(is_simple_point_query(&parse_select(
"SELECT no_o_id FROM new_order WHERE no_w_id = 1 ORDER BY no_o_id"
)));
assert!(is_simple_point_query(&parse_select(
"SELECT * FROM t WHERE id = 1 ORDER BY col ASC"
)));
assert!(is_simple_point_query(&parse_select(
"SELECT a, b FROM t WHERE x = 1 ORDER BY a DESC"
)));
assert!(is_simple_point_query(&parse_select("SELECT a FROM t WHERE x = 1 ORDER BY a, b")));
assert!(is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE x = 1 ORDER BY a DESC, b ASC"
)));
assert!(is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE x = 1 ORDER BY a LIMIT 1"
)));
}
#[test]
fn test_order_by_complex_not_simple() {
assert!(!is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE x = 1 ORDER BY UPPER(a)"
)));
assert!(!is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE x = 1 ORDER BY a + 1"
)));
assert!(!is_simple_point_query(&parse_select(
"SELECT a FROM t WHERE x = 1 ORDER BY COALESCE(a, b)"
)));
}
#[test]
fn test_needs_sorting() {
assert!(needs_sorting(
&[vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"a", false
)),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
}],
&None
));
assert!(!needs_sorting(
&[vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"a", false
)),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
}],
&Some(vec![("a".to_string(), vibesql_ast::OrderDirection::Asc)])
));
assert!(needs_sorting(
&[vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"b", false
)),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
}],
&Some(vec![("a".to_string(), vibesql_ast::OrderDirection::Asc)])
));
assert!(needs_sorting(
&[vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"a", false
)),
direction: vibesql_ast::OrderDirection::Desc,
nulls_order: None,
}],
&Some(vec![("a".to_string(), vibesql_ast::OrderDirection::Asc)])
));
assert!(!needs_sorting(
&[vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"a", false
)),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
}],
&Some(vec![
("a".to_string(), vibesql_ast::OrderDirection::Asc),
("b".to_string(), vibesql_ast::OrderDirection::Asc),
])
));
assert!(needs_sorting(
&[
vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::simple("a", false)
),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
},
vibesql_ast::OrderByItem {
expr: vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::simple("b", false)
),
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
},
],
&Some(vec![("a".to_string(), vibesql_ast::OrderDirection::Asc)])
));
}
#[test]
fn test_streaming_aggregate_detection() {
assert!(is_streaming_aggregate_query(&parse_select(
"SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(is_streaming_aggregate_query(&parse_select(
"SELECT COUNT(c) FROM sbtest1 WHERE id BETWEEN 50 AND 150"
)));
assert!(is_streaming_aggregate_query(&parse_select(
"SELECT AVG(k), SUM(k), COUNT(k) FROM sbtest1 WHERE id BETWEEN 1 AND 10"
)));
assert!(is_streaming_aggregate_query(&parse_select(
"SELECT MIN(k), MAX(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
}
#[test]
fn test_streaming_aggregate_negative_cases() {
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT c FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"WITH cte AS (SELECT 1) SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100 GROUP BY c"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT DISTINCT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100 ORDER BY 1"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN 1 AND 100 HAVING SUM(k) > 10"
)));
assert!(!is_streaming_aggregate_query(&parse_select("SELECT SUM(k) FROM sbtest1")));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT COUNT(DISTINCT k) FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT * FROM sbtest1 WHERE id BETWEEN 1 AND 100"
)));
assert!(!is_streaming_aggregate_query(&parse_select(
"SELECT SUM(a.k) FROM sbtest1 a, sbtest1 b WHERE a.id BETWEEN 1 AND 100"
)));
}
}