use vibesql_ast::{FromClause, SelectStmt};
use crate::{errors::ExecutorError, limits::MAX_TABLES_IN_JOIN};
fn count_tables_in_from_clause(from: &FromClause) -> usize {
match from {
FromClause::Table { .. } => 1,
FromClause::Join { left, right, .. } => {
count_tables_in_from_clause(left) + count_tables_in_from_clause(right)
}
FromClause::Subquery { .. } => 1, FromClause::Values { .. } => 1, }
}
pub fn validate_join_table_limit(stmt: &SelectStmt) -> Result<(), ExecutorError> {
if let Some(from_clause) = &stmt.from {
let table_count = count_tables_in_from_clause(from_clause);
if table_count > MAX_TABLES_IN_JOIN {
return Err(ExecutorError::JoinTableLimitExceeded {
table_count,
max_tables: MAX_TABLES_IN_JOIN,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use vibesql_ast::{Expression, SelectItem};
use vibesql_types::SqlValue;
use super::*;
#[test]
fn test_join_table_limit_under_limit() {
let mut from: FromClause = FromClause::Table {
index_hint: None,
name: "t1".to_string(),
alias: None,
column_aliases: None,
quoted: false,
};
for i in 2..=64 {
from = FromClause::Join {
left: Box::new(from),
right: Box::new(FromClause::Table {
index_hint: None,
name: format!("t{}", i),
alias: None,
column_aliases: None,
quoted: false,
}),
join_type: vibesql_ast::JoinType::Cross,
condition: None,
using_columns: None,
natural: false,
alias: None,
};
}
let stmt = SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![SelectItem::Wildcard { alias: None }],
into_table: None,
into_variables: None,
from: Some(from),
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
assert!(validate_join_table_limit(&stmt).is_ok());
}
#[test]
fn test_join_table_limit_exceeds_limit() {
let mut from: FromClause = FromClause::Table {
index_hint: None,
name: "t1".to_string(),
alias: None,
column_aliases: None,
quoted: false,
};
for i in 2..=65 {
from = FromClause::Join {
left: Box::new(from),
right: Box::new(FromClause::Table {
index_hint: None,
name: format!("t{}", i),
alias: None,
column_aliases: None,
quoted: false,
}),
join_type: vibesql_ast::JoinType::Cross,
condition: None,
using_columns: None,
natural: false,
alias: None,
};
}
let stmt = SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![SelectItem::Wildcard { alias: None }],
into_table: None,
into_variables: None,
from: Some(from),
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
let result = validate_join_table_limit(&stmt);
assert!(result.is_err());
match result {
Err(ExecutorError::JoinTableLimitExceeded { table_count, max_tables }) => {
assert_eq!(table_count, 65);
assert_eq!(max_tables, 64);
}
_ => panic!("Expected JoinTableLimitExceeded error"),
}
}
#[test]
fn test_join_table_limit_no_from_clause() {
let stmt = SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![SelectItem::Expression {
expr: Expression::Literal(SqlValue::Integer(1)),
alias: None,
source_text: None,
}],
into_table: None,
into_variables: None,
from: None,
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
assert!(validate_join_table_limit(&stmt).is_ok());
}
}