use std::collections::{HashMap, HashSet};
use vibesql_ast::{Expression, FromClause, SelectStmt};
use vibesql_storage::Database;
use crate::errors::ExecutorError;
pub const MAX_VIEW_REFERENCES: u64 = 65535;
const SATURATE_AT: u64 = MAX_VIEW_REFERENCES + 1;
pub(crate) fn check_view_reference_limit(
stmt: &SelectStmt,
database: &Database,
) -> Result<(), ExecutorError> {
let totals = view_reference_totals(stmt, database);
if let Some((name, _)) =
totals.iter().filter(|(_, c)| **c > MAX_VIEW_REFERENCES).max_by_key(|(_, c)| **c)
{
return Err(ExecutorError::Other(format!(
"too many references to \"{name}\": max {MAX_VIEW_REFERENCES}"
)));
}
Ok(())
}
pub(crate) fn max_view_reference_count(stmt: &SelectStmt, database: &Database) -> u64 {
view_reference_totals(stmt, database).values().copied().max().unwrap_or(0)
}
fn view_reference_totals(stmt: &SelectStmt, database: &Database) -> HashMap<String, u64> {
let mut top_level_views: Vec<String> = Vec::new();
collect_view_refs(stmt, database, &mut top_level_views);
if top_level_views.is_empty() {
return HashMap::new();
}
let mut subtree_memo: HashMap<String, HashMap<String, u64>> = HashMap::new();
let mut totals: HashMap<String, u64> = HashMap::new();
for view_name in &top_level_views {
let subtree = expand_view(view_name, database, &mut subtree_memo, &mut Vec::new());
for (name, count) in subtree {
let entry = totals.entry(name).or_insert(0);
*entry = entry.saturating_add(count).min(SATURATE_AT);
}
}
totals
}
fn expand_view(
view_name: &str,
database: &Database,
memo: &mut HashMap<String, HashMap<String, u64>>,
stack: &mut Vec<String>,
) -> HashMap<String, u64> {
let Some(view) = database.catalog.get_view(view_name) else {
return HashMap::new();
};
let canonical = view.name.clone();
if let Some(cached) = memo.get(&canonical) {
return cached.clone();
}
if stack.iter().any(|n| n.eq_ignore_ascii_case(&canonical)) {
let mut single = HashMap::new();
single.insert(canonical, 1);
return single;
}
let mut counts: HashMap<String, u64> = HashMap::new();
counts.insert(canonical.clone(), 1);
stack.push(canonical.clone());
let mut child_views: Vec<String> = Vec::new();
collect_view_refs(&view.query, database, &mut child_views);
let mut hit_cycle = false;
for child in &child_views {
let child_counts = expand_view(child, database, memo, stack);
for (name, count) in child_counts {
if name.eq_ignore_ascii_case(&canonical) {
hit_cycle = true;
}
let entry = counts.entry(name).or_insert(0);
*entry = entry.saturating_add(count).min(SATURATE_AT);
}
}
stack.pop();
if !hit_cycle {
memo.insert(canonical, counts.clone());
}
counts
}
fn collect_view_refs(stmt: &SelectStmt, database: &Database, out: &mut Vec<String>) {
let mut cte_names: HashSet<String> = HashSet::new();
if let Some(ctes) = &stmt.with_clause {
for cte in ctes {
cte_names.insert(cte.name.to_ascii_lowercase());
collect_view_refs(&cte.query, database, out);
}
}
if let Some(from) = &stmt.from {
collect_from_view_refs(from, database, &cte_names, out);
}
if let Some(set_op) = &stmt.set_operation {
collect_view_refs(&set_op.right, database, out);
}
for item in &stmt.select_list {
if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
collect_expr_view_refs(expr, database, out);
}
}
if let Some(where_clause) = &stmt.where_clause {
collect_expr_view_refs(where_clause, database, out);
}
if let Some(having) = &stmt.having {
collect_expr_view_refs(having, database, out);
}
}
fn collect_from_view_refs(
from: &FromClause,
database: &Database,
cte_names: &HashSet<String>,
out: &mut Vec<String>,
) {
match from {
FromClause::Table { name, .. } => {
if cte_names.contains(&name.to_ascii_lowercase()) {
return;
}
if database.catalog.get_view(name).is_some() {
out.push(name.clone());
}
}
FromClause::Subquery { query, .. } => {
collect_view_refs(query, database, out);
}
FromClause::Join { left, right, condition, .. } => {
collect_from_view_refs(left, database, cte_names, out);
collect_from_view_refs(right, database, cte_names, out);
if let Some(cond) = condition {
collect_expr_view_refs(cond, database, out);
}
}
FromClause::Values { rows, .. } => {
for row in rows {
for expr in row {
collect_expr_view_refs(expr, database, out);
}
}
}
}
}
fn collect_expr_view_refs(expr: &Expression, database: &Database, out: &mut Vec<String>) {
use vibesql_ast::visitor::{walk_expression, ExpressionVisitor, VisitResult};
struct SubqueryCollector<'a> {
database: &'a Database,
out: &'a mut Vec<String>,
}
impl ExpressionVisitor for SubqueryCollector<'_> {
fn pre_visit_expression(&mut self, expr: &Expression) -> VisitResult {
match expr {
Expression::ScalarSubquery(query)
| Expression::Exists { subquery: query, .. } => {
collect_view_refs(query, self.database, self.out);
VisitResult::Skip
}
Expression::In { subquery, .. }
| Expression::QuantifiedComparison { subquery, .. } => {
collect_view_refs(subquery, self.database, self.out);
VisitResult::Continue
}
_ => VisitResult::Continue,
}
}
}
let mut collector = SubqueryCollector { database, out };
walk_expression(&mut collector, expr);
}