use std::sync::{Arc, OnceLock};
use vibesql_ast::{
DeleteStmt, Expression, FromClause, SelectItem, SelectStmt, Statement, WhereClause,
};
#[derive(Debug, Clone)]
pub enum CachedPlan {
PkPointLookup(PkPointLookupPlan),
SimpleFastPath(SimpleFastPathPlan),
PkDelete(PkDeletePlan),
Standard,
}
impl CachedPlan {
pub fn is_fast_path(&self) -> bool {
!matches!(self, CachedPlan::Standard)
}
}
#[derive(Debug, Clone)]
pub struct SimpleFastPathPlan {
pub table_name: String,
resolved_columns: Arc<OnceLock<Arc<[String]>>>,
}
impl SimpleFastPathPlan {
pub fn new(table_name: String) -> Self {
Self { table_name, resolved_columns: Arc::new(OnceLock::new()) }
}
pub fn get_or_resolve_columns<F>(&self, resolver: F) -> Option<&Arc<[String]>>
where
F: FnOnce() -> Option<Vec<String>>,
{
self.resolved_columns.get_or_init(|| {
resolver().map(|v| v.into()).unwrap_or_else(|| Arc::from([]))
});
self.resolved_columns.get().filter(|cols| !cols.is_empty())
}
pub fn is_resolved(&self) -> bool {
self.resolved_columns.get().is_some()
}
}
#[derive(Debug, Clone)]
pub struct PkPointLookupPlan {
pub table_name: String,
pub pk_columns: Vec<String>,
pub param_to_pk_col: Vec<(usize, usize)>,
pub projection: ProjectionPlan,
resolved: Arc<OnceLock<ResolvedProjection>>,
}
#[derive(Debug, Clone)]
pub struct ResolvedProjection {
pub column_indices: Vec<usize>,
pub column_names: Arc<[String]>,
}
impl PkPointLookupPlan {
pub fn get_or_resolve<F>(&self, resolver: F) -> Option<&ResolvedProjection>
where
F: FnOnce(&ProjectionPlan) -> Option<ResolvedProjection>,
{
self.resolved.get_or_init(|| {
resolver(&self.projection).unwrap_or(ResolvedProjection {
column_indices: vec![],
column_names: Arc::from([]),
})
});
self.resolved.get().filter(|r| {
!r.column_names.is_empty() || matches!(self.projection, ProjectionPlan::Wildcard)
})
}
pub fn is_resolved(&self) -> bool {
self.resolved.get().is_some()
}
}
#[derive(Debug, Clone)]
pub enum ProjectionPlan {
Wildcard,
Columns(Vec<ColumnProjection>),
}
#[derive(Debug, Clone)]
pub struct ColumnProjection {
pub column_name: String,
pub alias: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PkDeletePlan {
pub table_name: String,
pub pk_columns: Vec<String>,
pub param_to_pk_col: Vec<(usize, usize)>,
fast_path_valid: Arc<OnceLock<bool>>,
}
impl PkDeletePlan {
pub fn new(
table_name: String,
pk_columns: Vec<String>,
param_to_pk_col: Vec<(usize, usize)>,
) -> Self {
Self { table_name, pk_columns, param_to_pk_col, fast_path_valid: Arc::new(OnceLock::new()) }
}
pub fn build_pk_values(
&self,
params: &[vibesql_types::SqlValue],
) -> Vec<vibesql_types::SqlValue> {
let mut pk_values = vec![vibesql_types::SqlValue::Null; self.pk_columns.len()];
for &(param_idx, pk_col_idx) in &self.param_to_pk_col {
if param_idx < params.len() && pk_col_idx < pk_values.len() {
pk_values[pk_col_idx] = params[param_idx].clone();
}
}
pk_values
}
pub fn is_fast_path_valid(&self) -> Option<bool> {
self.fast_path_valid.get().copied()
}
pub fn set_fast_path_valid(&self, valid: bool) -> bool {
*self.fast_path_valid.get_or_init(|| valid)
}
}
pub fn analyze_for_plan(stmt: &Statement) -> CachedPlan {
match stmt {
Statement::Select(select) => analyze_select(select),
Statement::Delete(delete) => analyze_delete(delete),
_ => CachedPlan::Standard,
}
}
fn analyze_select(stmt: &SelectStmt) -> CachedPlan {
if let Some(plan) = try_analyze_pk_lookup(stmt) {
return CachedPlan::PkPointLookup(plan);
}
if crate::select::is_simple_point_query(stmt) {
if let Some(table_name) = extract_single_table_name(stmt) {
return CachedPlan::SimpleFastPath(SimpleFastPathPlan::new(table_name.to_lowercase()));
}
}
CachedPlan::Standard
}
fn analyze_delete(stmt: &DeleteStmt) -> CachedPlan {
let where_clause = match &stmt.where_clause {
Some(WhereClause::Condition(expr)) => expr,
_ => return CachedPlan::Standard,
};
let param_mappings = match extract_pk_param_mappings(where_clause) {
Some(mappings) if !mappings.is_empty() => mappings,
_ => return CachedPlan::Standard,
};
let pk_columns: Vec<String> = param_mappings.iter().map(|(_, col)| col.clone()).collect();
let param_to_pk_col: Vec<(usize, usize)> = param_mappings
.iter()
.enumerate()
.map(|(pk_idx, (param_idx, _))| (*param_idx, pk_idx))
.collect();
CachedPlan::PkDelete(PkDeletePlan::new(
stmt.table_name.to_lowercase(),
pk_columns,
param_to_pk_col,
))
}
fn try_analyze_pk_lookup(stmt: &SelectStmt) -> Option<PkPointLookupPlan> {
let table_name = match &stmt.from {
Some(FromClause::Table { name, alias: None, .. }) => name.clone(),
_ => return None,
};
if stmt.with_clause.is_some()
|| stmt.set_operation.is_some()
|| stmt.group_by.is_some()
|| stmt.having.is_some()
|| stmt.distinct
|| stmt.order_by.is_some()
|| stmt.limit.is_some()
|| stmt.offset.is_some()
|| stmt.into_table.is_some()
|| stmt.into_variables.is_some()
{
return None;
}
let projection = analyze_select_list(&stmt.select_list)?;
let where_clause = stmt.where_clause.as_ref()?;
let param_mappings = extract_pk_param_mappings(where_clause)?;
if param_mappings.is_empty() {
return None;
}
let pk_columns: Vec<String> = param_mappings.iter().map(|(_, col)| col.clone()).collect();
let param_to_pk_col: Vec<(usize, usize)> = param_mappings
.iter()
.enumerate()
.map(|(pk_idx, (param_idx, _))| (*param_idx, pk_idx))
.collect();
Some(PkPointLookupPlan {
table_name: table_name.to_lowercase(),
pk_columns,
param_to_pk_col,
projection,
resolved: Arc::new(OnceLock::new()),
})
}
fn extract_single_table_name(stmt: &SelectStmt) -> Option<String> {
match &stmt.from {
Some(FromClause::Table { name, .. }) => Some(name.clone()),
_ => None,
}
}
fn analyze_select_list(select_list: &[SelectItem]) -> Option<ProjectionPlan> {
if select_list.len() == 1 {
if let SelectItem::Wildcard { .. } = &select_list[0] {
return Some(ProjectionPlan::Wildcard);
}
}
let mut columns = Vec::with_capacity(select_list.len());
for item in select_list {
match item {
SelectItem::Wildcard { .. } => {
return None;
}
SelectItem::QualifiedWildcard { .. } => {
return None;
}
SelectItem::Expression { expr, alias, .. } => {
let column_name = match expr {
Expression::ColumnRef(col_id) => col_id.column_canonical().to_string(),
_ => {
return None;
}
};
columns.push(ColumnProjection { column_name, alias: alias.clone() });
}
}
}
Some(ProjectionPlan::Columns(columns))
}
fn extract_pk_param_mappings(expr: &Expression) -> Option<Vec<(usize, String)>> {
let mut mappings = Vec::new();
collect_pk_param_mappings(expr, &mut mappings)?;
mappings.sort_by_key(|(idx, _)| *idx);
Some(mappings)
}
fn collect_pk_param_mappings(expr: &Expression, mappings: &mut Vec<(usize, String)>) -> Option<()> {
match expr {
Expression::BinaryOp { left, op, right } => {
match op {
vibesql_ast::BinaryOperator::And => {
collect_pk_param_mappings(left, mappings)?;
collect_pk_param_mappings(right, mappings)?;
}
vibesql_ast::BinaryOperator::Equal => {
if let Some(mapping) = extract_column_placeholder_pair(left, right) {
mappings.push(mapping);
} else {
return None;
}
}
_ => {
return None;
}
}
}
_ => {
return None;
}
}
Some(())
}
fn extract_column_placeholder_pair(
left: &Expression,
right: &Expression,
) -> Option<(usize, String)> {
if let Expression::ColumnRef(col_id) = left {
if let Expression::Placeholder(idx) = right {
return Some((*idx, col_id.column_canonical().to_string()));
}
}
if let Expression::Placeholder(idx) = left {
if let Expression::ColumnRef(col_id) = right {
return Some((*idx, col_id.column_canonical().to_string()));
}
}
None
}
#[cfg(test)]
mod tests {
use vibesql_parser::Parser;
use super::*;
fn parse_to_plan(sql: &str) -> CachedPlan {
let stmt = Parser::parse_sql(sql).unwrap();
analyze_for_plan(&stmt)
}
#[test]
fn test_simple_pk_lookup() {
let plan = parse_to_plan("SELECT * FROM users WHERE id = ?");
match plan {
CachedPlan::PkPointLookup(p) => {
assert_eq!(p.table_name, "users");
assert_eq!(p.pk_columns, vec!["id"]);
assert_eq!(p.param_to_pk_col, vec![(0, 0)]);
assert!(matches!(p.projection, ProjectionPlan::Wildcard));
}
_ => panic!("Expected PkPointLookup"),
}
}
#[test]
fn test_composite_pk_lookup() {
let plan = parse_to_plan("SELECT * FROM orders WHERE customer_id = ? AND order_id = ?");
match plan {
CachedPlan::PkPointLookup(p) => {
assert_eq!(p.table_name, "orders");
assert_eq!(p.pk_columns, vec!["customer_id", "order_id"]);
assert_eq!(p.param_to_pk_col.len(), 2);
}
_ => panic!("Expected PkPointLookup"),
}
}
#[test]
fn test_projected_columns() {
let plan = parse_to_plan("SELECT name, email FROM users WHERE id = ?");
match plan {
CachedPlan::PkPointLookup(p) => {
match p.projection {
ProjectionPlan::Columns(cols) => {
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].column_name, "name");
assert_eq!(cols[1].column_name, "email");
}
_ => panic!("Expected Columns projection"),
}
}
_ => panic!("Expected PkPointLookup"),
}
}
#[test]
fn test_not_cacheable_join() {
let plan =
parse_to_plan("SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = ?");
assert!(matches!(plan, CachedPlan::Standard));
}
#[test]
fn test_not_cacheable_aggregate() {
let plan = parse_to_plan("SELECT COUNT(*) FROM users WHERE id = ?");
assert!(matches!(plan, CachedPlan::Standard));
}
#[test]
fn test_not_cacheable_order_by() {
let plan = parse_to_plan("SELECT * FROM users WHERE id = ? ORDER BY name");
assert!(matches!(plan, CachedPlan::Standard));
}
#[test]
fn test_not_cacheable_or() {
let plan = parse_to_plan("SELECT * FROM users WHERE id = ? OR name = ?");
assert!(matches!(plan, CachedPlan::Standard));
}
#[test]
fn test_literal_gets_simple_fast_path() {
let plan = parse_to_plan("SELECT * FROM users WHERE id = 1");
assert!(matches!(plan, CachedPlan::SimpleFastPath(_)));
}
#[test]
fn test_delete_pk_lookup() {
let plan = parse_to_plan("DELETE FROM sbtest1 WHERE id = ?");
match plan {
CachedPlan::PkDelete(p) => {
assert_eq!(p.table_name, "sbtest1");
assert_eq!(p.pk_columns, vec!["id"]);
assert_eq!(p.param_to_pk_col, vec![(0, 0)]);
}
other => panic!("Expected PkDelete, got {:?}", other),
}
}
#[test]
fn test_delete_without_where_not_fast_path() {
let plan = parse_to_plan("DELETE FROM users");
assert!(matches!(plan, CachedPlan::Standard));
}
}