use std::{cell::RefCell, collections::HashSet};
use super::{
super::super::core::{CombinedExpressionEvaluator, ExpressionEvaluator},
schema_utils::{
build_merged_outer_row, build_merged_outer_schema, compute_select_list_column_count,
get_subquery_first_column_affinity,
},
};
use crate::errors::ExecutorError;
use vibesql_types::TypeAffinity;
#[derive(Clone)]
struct InSubqueryHashSetEntry {
has_null: bool,
value_set: HashSet<vibesql_types::SqlValue>,
}
thread_local! {
static IN_SUBQUERY_HASHSET_CACHE: RefCell<lru::LruCache<u64, InSubqueryHashSetEntry>> =
RefCell::new(lru::LruCache::new(std::num::NonZeroUsize::new(1000).unwrap()));
}
pub fn clear_in_subquery_cache() {
IN_SUBQUERY_HASHSET_CACHE.with(|cache| {
cache.borrow_mut().clear();
});
}
fn build_hashset_from_rows(rows: &[vibesql_storage::Row]) -> InSubqueryHashSetEntry {
let mut has_null = false;
let mut value_set = HashSet::with_capacity(rows.len());
for row in rows {
if let Some(val) = row.values.first() {
if matches!(val, vibesql_types::SqlValue::Null) {
has_null = true;
} else {
value_set.insert(val.clone());
}
}
}
InSubqueryHashSetEntry { has_null, value_set }
}
impl CombinedExpressionEvaluator<'_> {
pub(in crate::evaluator::combined) fn eval_in_subquery(
&self,
expr: &vibesql_ast::Expression,
subquery: &vibesql_ast::SelectStmt,
negated: bool,
row: &vibesql_storage::Row,
) -> Result<vibesql_types::SqlValue, ExecutorError> {
if self.depth >= crate::limits::MAX_EXPRESSION_DEPTH {
return Err(ExecutorError::ExpressionDepthExceeded {
depth: self.depth,
max_depth: crate::limits::MAX_EXPRESSION_DEPTH,
});
}
let database = self.database.ok_or(ExecutorError::UnsupportedFeature(
"IN with subquery requires database reference".to_string(),
))?;
let sql_mode = database.sql_mode();
if let vibesql_ast::Expression::RowValueConstructor(expr_elements) = expr {
return self.eval_row_value_in_subquery(
expr_elements,
subquery,
negated,
row,
database,
);
}
let expr_val = self.eval(expr, row)?;
let left_affinity = self.get_expression_affinity(expr);
let subquery_affinity = get_subquery_first_column_affinity(subquery, database);
if can_use_index_for_in_subquery(subquery, database) {
let index_result_opt = try_index_optimized_in_subquery(
&expr_val,
subquery,
negated,
database,
sql_mode.clone(),
left_affinity,
subquery_affinity,
)?;
if let Some(index_result) = index_result_opt {
return Ok(index_result);
}
}
let is_correlated = self.is_correlated_cached(subquery);
if !is_correlated {
let cache_key = self.compute_subquery_hash_cached(subquery);
let hashset_entry = IN_SUBQUERY_HASHSET_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(entry) = cache.get(&cache_key) {
return Some(entry.clone());
}
if let Some(rows) = self.subquery_cache.borrow().peek(&cache_key) {
let column_count = if !rows.is_empty() {
rows[0].values.len()
} else {
1
};
if column_count == 1 {
let entry = build_hashset_from_rows(rows);
cache.put(cache_key, entry.clone());
return Some(entry);
}
}
None
});
if let Some(entry) = hashset_entry {
return eval_in_with_hashset(
&expr_val,
&entry,
negated,
sql_mode,
left_affinity,
subquery_affinity,
);
}
let cached_result = self.subquery_cache.borrow().peek(&cache_key).cloned();
let rows = if let Some(cached_rows) = cached_result {
cached_rows
} else {
let select_executor = if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_cte_and_depth(
database, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_depth(database, self.depth)
};
match select_executor.execute(subquery) {
Ok(rows) => {
self.subquery_cache.borrow_mut().put(cache_key, rows.clone());
rows
}
Err(ExecutorError::ColumnNotFound { .. }) => {
let merged_schema = if !self.schema.table_schemas.is_empty()
|| self.outer_schema.is_some()
{
Some(build_merged_outer_schema(self.schema, self.outer_schema))
} else {
None
};
let merged_row = if merged_schema.is_some() {
Some(build_merged_outer_row(row, self.outer_row))
} else {
None
};
let retry_executor = if let (Some(ref schema), Some(ref outer_row)) =
(&merged_schema, &merged_row)
{
if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_outer_and_cte_and_depth(
database, outer_row, schema, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_outer_context_and_depth(
database, outer_row, schema, self.depth,
)
}
} else if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_cte_and_depth(
database, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new(database)
};
let rows = retry_executor.execute(subquery)?;
let column_count = if !rows.is_empty() {
rows[0].values.len()
} else {
compute_select_list_column_count(subquery, database, self.cte_context)?
};
if column_count != 1 {
return Err(ExecutorError::SubqueryColumnCountMismatch {
expected: 1,
actual: column_count,
});
}
return eval_in_linear(
&expr_val,
&rows,
negated,
sql_mode,
left_affinity,
subquery_affinity,
);
}
Err(e) => return Err(e),
}
};
let column_count = if !rows.is_empty() {
rows[0].values.len()
} else {
compute_select_list_column_count(subquery, database, self.cte_context)?
};
if column_count != 1 {
return Err(ExecutorError::SubqueryColumnCountMismatch {
expected: 1,
actual: column_count,
});
}
let entry = build_hashset_from_rows(&rows);
IN_SUBQUERY_HASHSET_CACHE.with(|cache| {
cache.borrow_mut().put(cache_key, entry.clone());
});
return eval_in_with_hashset(
&expr_val,
&entry,
negated,
sql_mode,
left_affinity,
subquery_affinity,
);
}
let merged_schema = if !self.schema.table_schemas.is_empty() || self.outer_schema.is_some()
{
Some(build_merged_outer_schema(self.schema, self.outer_schema))
} else {
None
};
let merged_row = if merged_schema.is_some() {
Some(build_merged_outer_row(row, self.outer_row))
} else {
None
};
let select_executor =
if let (Some(ref schema), Some(ref outer_row)) = (&merged_schema, &merged_row) {
if let Some(outer_rows) = self.outer_rows {
if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_outer_rows_and_cte_and_depth(
database, outer_row, schema, outer_rows, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_outer_rows_and_depth(
database, outer_row, schema, outer_rows, self.depth,
)
}
} else if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_outer_and_cte_and_depth(
database, outer_row, schema, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_outer_context_and_depth(
database, outer_row, schema, self.depth,
)
}
} else if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_cte_and_depth(database, cte_ctx, self.depth)
} else {
crate::select::SelectExecutor::new(database)
};
let rows = select_executor.execute(subquery)?;
let column_count = if !rows.is_empty() {
rows[0].values.len()
} else {
compute_select_list_column_count(subquery, database, self.cte_context)?
};
if column_count != 1 {
return Err(ExecutorError::SubqueryColumnCountMismatch {
expected: 1,
actual: column_count,
});
}
eval_in_linear(&expr_val, &rows, negated, sql_mode, left_affinity, subquery_affinity)
}
fn eval_row_value_in_subquery(
&self,
expr_elements: &[vibesql_ast::Expression],
subquery: &vibesql_ast::SelectStmt,
negated: bool,
row: &vibesql_storage::Row,
database: &vibesql_storage::Database,
) -> Result<vibesql_types::SqlValue, ExecutorError> {
use vibesql_types::SqlValue;
let expected_columns = expr_elements.len();
if expected_columns == 0 {
return Err(ExecutorError::UnsupportedExpression(
"Empty row value constructors are not allowed".to_string(),
));
}
let mut expr_values = Vec::with_capacity(expected_columns);
for elem_expr in expr_elements {
let val = self.eval(elem_expr, row)?;
expr_values.push(val);
}
let merged_schema = if !self.schema.table_schemas.is_empty() || self.outer_schema.is_some()
{
Some(build_merged_outer_schema(self.schema, self.outer_schema))
} else {
None
};
let merged_row = if merged_schema.is_some() {
Some(build_merged_outer_row(row, self.outer_row))
} else {
None
};
let select_executor =
if let (Some(ref schema), Some(ref outer_row)) = (&merged_schema, &merged_row) {
if let Some(outer_rows) = self.outer_rows {
if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_outer_rows_and_cte_and_depth(
database, outer_row, schema, outer_rows, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_outer_rows_and_depth(
database, outer_row, schema, outer_rows, self.depth,
)
}
} else if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_outer_and_cte_and_depth(
database, outer_row, schema, cte_ctx, self.depth,
)
} else {
crate::select::SelectExecutor::new_with_outer_context_and_depth(
database, outer_row, schema, self.depth,
)
}
} else if let Some(cte_ctx) = self.cte_context {
crate::select::SelectExecutor::new_with_cte_and_depth(database, cte_ctx, self.depth)
} else {
crate::select::SelectExecutor::new_with_depth(database, self.depth)
};
let rows = select_executor.execute(subquery)?;
let column_count = if !rows.is_empty() {
rows[0].values.len()
} else {
compute_select_list_column_count(subquery, database, self.cte_context)?
};
if column_count != expected_columns {
return Err(ExecutorError::SubqueryColumnCountMismatch {
expected: expected_columns,
actual: column_count,
});
}
if rows.is_empty() {
return Ok(SqlValue::Boolean(negated));
}
let sql_mode = database.sql_mode();
let mut found_unknown_row = false;
for subquery_row in &rows {
let mut all_equal = true;
let mut has_null_comparison = false;
for (i, expr_val) in expr_values.iter().enumerate() {
let subquery_val = subquery_row
.get(i)
.ok_or(ExecutorError::ColumnIndexOutOfBounds { index: i })?;
if matches!(expr_val, SqlValue::Null) || matches!(subquery_val, SqlValue::Null) {
has_null_comparison = true;
continue;
}
let eq_result = ExpressionEvaluator::eval_binary_op_static(
expr_val,
&vibesql_ast::BinaryOperator::Equal,
subquery_val,
sql_mode.clone(),
)?;
match eq_result {
SqlValue::Boolean(true) => {
}
SqlValue::Boolean(false) => {
all_equal = false;
break;
}
SqlValue::Null => {
has_null_comparison = true;
}
_ => {
return Err(ExecutorError::TypeError(format!(
"Comparison returned non-boolean: {:?}",
eq_result
)));
}
}
}
if all_equal {
if !has_null_comparison {
return Ok(SqlValue::Boolean(!negated));
}
found_unknown_row = true;
}
}
if found_unknown_row {
Ok(SqlValue::Null)
} else {
Ok(SqlValue::Boolean(negated))
}
}
}
fn eval_in_with_hashset(
expr_val: &vibesql_types::SqlValue,
entry: &InSubqueryHashSetEntry,
negated: bool,
sql_mode: vibesql_types::SqlMode,
left_affinity: Option<TypeAffinity>,
subquery_affinity: Option<TypeAffinity>,
) -> Result<vibesql_types::SqlValue, ExecutorError> {
if matches!(expr_val, vibesql_types::SqlValue::Null) {
if entry.value_set.is_empty() && !entry.has_null {
return Ok(vibesql_types::SqlValue::Boolean(negated));
}
return Ok(vibesql_types::SqlValue::Null);
}
if entry.value_set.contains(expr_val) {
return Ok(vibesql_types::SqlValue::Boolean(!negated));
}
for value in &entry.value_set {
let (coerced_expr, coerced_value) = apply_in_subquery_affinity_coercion(
expr_val.clone(),
value.clone(),
left_affinity,
subquery_affinity,
);
if coerced_expr == coerced_value {
return Ok(vibesql_types::SqlValue::Boolean(!negated));
}
if std::mem::discriminant(&coerced_expr) != std::mem::discriminant(&coerced_value) {
let eq_result = ExpressionEvaluator::eval_binary_op_static(
&coerced_expr,
&vibesql_ast::BinaryOperator::Equal,
&coerced_value,
sql_mode.clone(),
)?;
if matches!(eq_result, vibesql_types::SqlValue::Boolean(true)) {
return Ok(vibesql_types::SqlValue::Boolean(!negated));
}
}
}
if entry.has_null {
Ok(vibesql_types::SqlValue::Null)
} else {
Ok(vibesql_types::SqlValue::Boolean(negated))
}
}
fn eval_in_linear(
expr_val: &vibesql_types::SqlValue,
rows: &[vibesql_storage::Row],
negated: bool,
sql_mode: vibesql_types::SqlMode,
left_affinity: Option<TypeAffinity>,
subquery_affinity: Option<TypeAffinity>,
) -> Result<vibesql_types::SqlValue, ExecutorError> {
if matches!(expr_val, vibesql_types::SqlValue::Null) {
if rows.is_empty() {
return Ok(vibesql_types::SqlValue::Boolean(negated));
}
for row in rows {
if let Some(val) = row.values.first() {
if matches!(val, vibesql_types::SqlValue::Null) {
return Ok(vibesql_types::SqlValue::Null);
}
}
}
return Ok(vibesql_types::SqlValue::Null);
}
let mut found_null = false;
for row in rows {
let subquery_val = row.get(0).ok_or(ExecutorError::ColumnIndexOutOfBounds { index: 0 })?;
if matches!(subquery_val, vibesql_types::SqlValue::Null) {
found_null = true;
continue;
}
let (coerced_expr, coerced_subquery) = apply_in_subquery_affinity_coercion(
expr_val.clone(),
subquery_val.clone(),
left_affinity,
subquery_affinity,
);
if coerced_expr == coerced_subquery {
return Ok(vibesql_types::SqlValue::Boolean(!negated));
}
let eq_result = ExpressionEvaluator::eval_binary_op_static(
&coerced_expr,
&vibesql_ast::BinaryOperator::Equal,
&coerced_subquery,
sql_mode.clone(),
)?;
if matches!(eq_result, vibesql_types::SqlValue::Boolean(true)) {
return Ok(vibesql_types::SqlValue::Boolean(!negated));
}
}
if found_null {
Ok(vibesql_types::SqlValue::Null)
} else {
Ok(vibesql_types::SqlValue::Boolean(negated))
}
}
fn can_use_index_for_in_subquery(
subquery: &vibesql_ast::SelectStmt,
database: &vibesql_storage::Database,
) -> bool {
let table_name = match &subquery.from {
Some(vibesql_ast::FromClause::Table { name, .. }) => name,
_ => return false, };
if subquery.group_by.is_some()
|| subquery.having.is_some()
|| subquery.limit.is_some()
|| subquery.offset.is_some()
|| subquery.distinct
{
return false;
}
if subquery.select_list.len() != 1 {
return false;
}
#[allow(clippy::collapsible_match)]
let column_name = match &subquery.select_list[0] {
vibesql_ast::SelectItem::Expression { expr, .. } => {
match expr {
vibesql_ast::Expression::ColumnRef(col_id) => col_id.column_canonical(),
_ => return false, }
}
_ => return false, };
let indexes = database.list_indexes_for_table(table_name);
for index_name in &indexes {
if let Some(index_metadata) = database.get_index(index_name) {
if database
.catalog
.find_index_by_name(index_name)
.map(|m| m.is_partial())
.unwrap_or(false)
{
continue;
}
if let Some(first_col) = index_metadata.columns.first() {
if first_col.expect_column_name() == column_name {
return true;
}
}
}
}
false
}
fn try_index_optimized_in_subquery(
expr_val: &vibesql_types::SqlValue,
subquery: &vibesql_ast::SelectStmt,
negated: bool,
database: &vibesql_storage::Database,
sql_mode: vibesql_types::SqlMode,
left_affinity: Option<TypeAffinity>,
subquery_affinity: Option<TypeAffinity>,
) -> Result<Option<vibesql_types::SqlValue>, ExecutorError> {
let table_name = match &subquery.from {
Some(vibesql_ast::FromClause::Table { name, .. }) => name,
_ => return Ok(None),
};
#[allow(clippy::collapsible_match)]
let column_name = match &subquery.select_list[0] {
vibesql_ast::SelectItem::Expression { expr, .. } => match expr {
vibesql_ast::Expression::ColumnRef(col_id) => col_id.column_canonical(),
_ => return Ok(None),
},
_ => return Ok(None),
};
let indexes = database.list_indexes_for_table(table_name);
let mut selected_index: Option<String> = None;
for index_name in &indexes {
if let Some(index_metadata) = database.get_index(index_name) {
if database
.catalog
.find_index_by_name(index_name)
.map(|m| m.is_partial())
.unwrap_or(false)
{
continue;
}
if let Some(first_col) = index_metadata.columns.first() {
if first_col.expect_column_name() == column_name {
selected_index = Some(index_name.clone());
break;
}
}
}
}
let index_name = match selected_index {
Some(name) => name,
None => return Ok(None),
};
let index_data = match database.get_index_data(&index_name) {
Some(data) => data,
None => return Ok(None),
};
let table = match database.get_table(table_name) {
Some(t) => t,
None => return Ok(None),
};
let values_set = if let Some(where_expr) = &subquery.where_clause {
use crate::select::scan::index_scan::predicate::extract_index_predicate;
if let Some(index_pred) = extract_index_predicate(where_expr, column_name) {
let row_indices: Vec<usize> = match index_pred {
crate::select::scan::index_scan::predicate::IndexPredicate::Range(range) => {
index_data.range_scan(
range.start.as_ref(),
range.end.as_ref(),
range.inclusive_start,
range.inclusive_end,
)
}
crate::select::scan::index_scan::predicate::IndexPredicate::In(vals) => {
index_data.multi_lookup(&vals)
}
};
let all_rows = table.scan();
let column_index =
table.schema.columns.iter().position(|col| col.name == *column_name).ok_or_else(
|| ExecutorError::ColumnNotFound {
column_name: column_name.to_string(),
table_name: table_name.clone(),
searched_tables: vec![table_name.clone()],
available_columns: table
.schema
.columns
.iter()
.map(|c| c.name.clone())
.collect(),
},
)?;
let mut values = std::collections::HashSet::new();
for row_idx in row_indices {
if let Some(row) = all_rows.get(row_idx) {
if let Some(value) = row.values.get(column_index) {
values.insert(value.clone());
}
}
}
values
} else {
return Ok(None);
}
} else {
let all_rows = table.scan();
let column_index = table
.schema
.columns
.iter()
.position(|col| col.name == *column_name)
.ok_or_else(|| ExecutorError::ColumnNotFound {
column_name: column_name.to_string(),
table_name: table_name.clone(),
searched_tables: vec![table_name.clone()],
available_columns: table.schema.columns.iter().map(|c| c.name.clone()).collect(),
})?;
let mut values = std::collections::HashSet::new();
for row in all_rows {
if let Some(value) = row.values.get(column_index) {
values.insert(value.clone());
}
}
values
};
if matches!(expr_val, vibesql_types::SqlValue::Null) {
if values_set.is_empty() {
return Ok(Some(vibesql_types::SqlValue::Boolean(negated)));
}
if values_set.contains(&vibesql_types::SqlValue::Null) {
return Ok(Some(vibesql_types::SqlValue::Null));
}
return Ok(Some(vibesql_types::SqlValue::Null));
}
let mut found_null = false;
for value in &values_set {
if matches!(value, vibesql_types::SqlValue::Null) {
found_null = true;
continue;
}
let (coerced_expr, coerced_value) = apply_in_subquery_affinity_coercion(
expr_val.clone(),
value.clone(),
left_affinity,
subquery_affinity,
);
if coerced_expr == coerced_value {
return Ok(Some(vibesql_types::SqlValue::Boolean(!negated)));
}
let eq_result = ExpressionEvaluator::eval_binary_op_static(
&coerced_expr,
&vibesql_ast::BinaryOperator::Equal,
&coerced_value,
sql_mode.clone(),
)?;
if matches!(eq_result, vibesql_types::SqlValue::Boolean(true)) {
return Ok(Some(vibesql_types::SqlValue::Boolean(!negated)));
}
}
if found_null {
Ok(Some(vibesql_types::SqlValue::Null))
} else {
Ok(Some(vibesql_types::SqlValue::Boolean(negated)))
}
}
fn apply_in_subquery_affinity_coercion(
left_val: vibesql_types::SqlValue,
right_val: vibesql_types::SqlValue,
left_affinity: Option<TypeAffinity>,
right_affinity: Option<TypeAffinity>,
) -> (vibesql_types::SqlValue, vibesql_types::SqlValue) {
use vibesql_types::SqlValue;
let right_is_numeric_affinity = matches!(
right_affinity,
Some(TypeAffinity::Integer) | Some(TypeAffinity::Real) | Some(TypeAffinity::Numeric)
);
if right_is_numeric_affinity {
if let SqlValue::Varchar(s) | SqlValue::Character(s) = &left_val {
if let Some(coerced) = try_coerce_string_to_numeric(s) {
return (coerced, right_val);
}
}
}
let left_is_numeric_affinity = matches!(
left_affinity,
Some(TypeAffinity::Integer) | Some(TypeAffinity::Real) | Some(TypeAffinity::Numeric)
);
if left_is_numeric_affinity {
if let SqlValue::Varchar(s) | SqlValue::Character(s) = &right_val {
if let Some(coerced) = try_coerce_string_to_numeric(s) {
return (left_val, coerced);
}
}
}
if matches!(right_affinity, Some(TypeAffinity::Text)) {
if let SqlValue::Integer(n) = &left_val {
return (SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())), right_val);
}
if let SqlValue::Real(n) | SqlValue::Double(n) = &left_val {
return (SqlValue::Varchar(arcstr::ArcStr::from(format_float_for_text(*n))), right_val);
}
}
if matches!(left_affinity, Some(TypeAffinity::Text)) {
if let SqlValue::Integer(n) = &right_val {
return (left_val, SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())));
}
if let SqlValue::Real(n) | SqlValue::Double(n) = &right_val {
return (left_val, SqlValue::Varchar(arcstr::ArcStr::from(format_float_for_text(*n))));
}
}
(left_val, right_val)
}
fn try_coerce_string_to_numeric(s: &str) -> Option<vibesql_types::SqlValue> {
if let Ok(n) = s.parse::<i64>() {
return Some(vibesql_types::SqlValue::Integer(n));
}
if let Ok(n) = s.parse::<f64>() {
return Some(vibesql_types::SqlValue::Real(n));
}
None
}
fn format_float_for_text(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
format!("{}.0", n as i64)
} else {
n.to_string()
}
}