use vibesql_ast::{CreateIndexStmt, Expression, IndexColumn};
use vibesql_catalog::TableSchema;
use vibesql_storage::Database;
use crate::{
errors::ExecutorError,
privilege_checker::PrivilegeChecker,
sqlite_schema::{is_reserved_object_name, is_sqlite_schema_table},
};
pub struct ValidationResult {
pub table_name: String,
pub qualified_table_name: String,
pub table_schema: TableSchema,
}
pub fn validate_create_index(
stmt: &CreateIndexStmt,
database: &Database,
) -> Result<ValidationResult, ExecutorError> {
let (schema_name, table_name) =
if let Some((schema_part, table_part)) = stmt.table_name.split_once('.') {
(schema_part.to_string(), table_part.to_string())
} else {
let resolved_schema = database
.catalog
.resolve_table_schema_name(&stmt.table_name)
.unwrap_or_else(|| database.catalog.get_current_schema().to_string());
(resolved_schema, stmt.table_name.clone())
};
if is_sqlite_schema_table(&table_name) {
return Err(ExecutorError::SqliteSystemTableReadOnly {
table_name: table_name.clone(),
operation: "indexed".to_string(),
});
}
if is_reserved_object_name(&stmt.index_name) {
return Err(ExecutorError::SqliteCompatError(format!(
"object name reserved for internal use: {}",
stmt.index_name
)));
}
PrivilegeChecker::check_create(database, &schema_name)?;
let qualified_table_name = format!("{}.{}", schema_name, table_name);
if !database.catalog.table_exists(&qualified_table_name) {
return Err(ExecutorError::TableNotFound(qualified_table_name.clone()));
}
let table_schema = database
.catalog
.get_table(&qualified_table_name)
.ok_or_else(|| ExecutorError::TableNotFound(qualified_table_name.clone()))?
.clone();
validate_no_window_functions_in_index(stmt)?;
validate_expression_determinism(&stmt.columns)?;
validate_indexed_columns(&stmt.columns, &table_schema, &qualified_table_name)?;
validate_prefix_lengths(&stmt.columns, &table_schema)?;
check_index_exists(stmt, database, &schema_name)?;
check_namespace_collision(&stmt.index_name, database)?;
Ok(ValidationResult { table_name, qualified_table_name, table_schema })
}
fn validate_no_window_functions_in_index(stmt: &CreateIndexStmt) -> Result<(), ExecutorError> {
for index_col in &stmt.columns {
if let Some(expr) = index_col.get_expression() {
if let Some(window_name) = crate::select::find_window_function_in_expression(expr) {
return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
}
}
}
if let Some(where_expr) = &stmt.where_clause {
if let Some(window_name) = crate::select::find_window_function_in_expression(where_expr) {
return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
}
}
Ok(())
}
fn validate_expression_determinism(columns: &[IndexColumn]) -> Result<(), ExecutorError> {
for index_col in columns {
if let Some(expr) = index_col.get_expression() {
if !crate::evaluator::expression_hash::ExpressionHasher::is_deterministic_for_index(
expr,
) {
return Err(ExecutorError::UnsupportedFeature(
"Expression indexes must use deterministic expressions. \
Non-deterministic functions like RANDOM(), CURRENT_TIMESTAMP, etc. \
are not allowed."
.to_string(),
));
}
}
}
Ok(())
}
fn validate_indexed_columns(
columns: &[IndexColumn],
table_schema: &TableSchema,
qualified_table_name: &str,
) -> Result<(), ExecutorError> {
for index_col in columns {
if let Some(col_name) = index_col.column_name() {
if table_schema.get_column(col_name).is_none() {
let available_columns =
table_schema.columns.iter().map(|c| c.name.clone()).collect();
return Err(ExecutorError::ColumnNotFound {
column_name: col_name.to_string(),
table_name: qualified_table_name.to_string(),
searched_tables: vec![qualified_table_name.to_string()],
available_columns,
});
}
}
if let Some(expr) = index_col.get_expression() {
validate_expression_columns(expr, table_schema, qualified_table_name)?;
}
}
Ok(())
}
fn validate_prefix_lengths(
columns: &[IndexColumn],
table_schema: &TableSchema,
) -> Result<(), ExecutorError> {
for index_col in columns {
if let Some(prefix_len) = index_col.prefix_length() {
if index_col.is_expression() {
return Err(ExecutorError::InvalidIndexDefinition(
"Prefix length cannot be specified for expression indexes".to_string(),
));
}
let col_name = index_col.column_name().unwrap();
if prefix_len == 0 {
return Err(ExecutorError::InvalidIndexDefinition(format!(
"Prefix length must be greater than 0 for column '{}'",
col_name
)));
}
let column = table_schema.get_column(col_name).unwrap(); match column.data_type {
vibesql_types::DataType::Varchar { .. }
| vibesql_types::DataType::Character { .. } => {
}
_ => {
return Err(ExecutorError::InvalidIndexDefinition(format!(
"Prefix length can only be specified for string columns, but column '{}' has type {:?}",
col_name, column.data_type
)));
}
}
const MAX_PREFIX_LENGTH: u64 = 65536;
if prefix_len > MAX_PREFIX_LENGTH {
return Err(ExecutorError::InvalidIndexDefinition(format!(
"Prefix length {} is too large for column '{}' (maximum: {})",
prefix_len, col_name, MAX_PREFIX_LENGTH
)));
}
}
}
Ok(())
}
fn check_index_exists(
stmt: &CreateIndexStmt,
database: &Database,
schema_name: &str,
) -> Result<(), ExecutorError> {
let index_name = &stmt.index_name;
let qualified = format!("{}.{}", schema_name, index_name);
let index_exists =
database.get_index(&qualified).is_some() || database.spatial_index_exists(&qualified);
if index_exists {
if stmt.if_not_exists {
return Ok(());
} else {
return Err(ExecutorError::IndexAlreadyExists(index_name.clone()));
}
}
Ok(())
}
fn check_namespace_collision(index_name: &str, database: &Database) -> Result<(), ExecutorError> {
let normalized_index_name = index_name.to_lowercase();
for schema in database.catalog.list_schemas() {
let qualified_name = format!("{}.{}", schema, normalized_index_name);
if database.catalog.table_exists(&qualified_name) {
return Err(ExecutorError::SqliteCompatError(format!(
"there is already a table named {}",
index_name
)));
}
}
Ok(())
}
pub fn index_already_exists(stmt: &CreateIndexStmt, database: &Database) -> bool {
let index_name = &stmt.index_name;
let schema_name = if let Some((schema_part, _)) = stmt.table_name.split_once('.') {
schema_part.to_string()
} else {
database
.catalog
.resolve_table_schema_name(&stmt.table_name)
.unwrap_or_else(|| database.catalog.get_current_schema().to_string())
};
let qualified = format!("{}.{}", schema_name, index_name);
database.get_index(&qualified).is_some() || database.spatial_index_exists(&qualified)
}
pub fn validate_expression_columns(
expr: &Expression,
table_schema: &TableSchema,
qualified_table_name: &str,
) -> Result<(), ExecutorError> {
match expr {
Expression::ColumnRef(col_id) => {
let col_name = col_id.column_canonical();
if table_schema.get_column(col_name).is_none() {
let available_columns =
table_schema.columns.iter().map(|c| c.name.clone()).collect();
return Err(ExecutorError::ColumnNotFound {
column_name: col_name.to_string(),
table_name: qualified_table_name.to_string(),
searched_tables: vec![qualified_table_name.to_string()],
available_columns,
});
}
Ok(())
}
Expression::BinaryOp { left, right, .. } => {
validate_expression_columns(left, table_schema, qualified_table_name)?;
validate_expression_columns(right, table_schema, qualified_table_name)
}
Expression::UnaryOp { expr, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)
}
Expression::Function { args, .. } => {
for arg in args {
validate_expression_columns(arg, table_schema, qualified_table_name)?;
}
Ok(())
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_expression_columns(op, table_schema, qualified_table_name)?;
}
for clause in when_clauses {
for cond in &clause.conditions {
validate_expression_columns(cond, table_schema, qualified_table_name)?;
}
validate_expression_columns(&clause.result, table_schema, qualified_table_name)?;
}
if let Some(else_expr) = else_result {
validate_expression_columns(else_expr, table_schema, qualified_table_name)?;
}
Ok(())
}
Expression::Cast { expr, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)
}
Expression::Collate { expr, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)
}
Expression::IsNull { expr, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)
}
Expression::Between { expr, low, high, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)?;
validate_expression_columns(low, table_schema, qualified_table_name)?;
validate_expression_columns(high, table_schema, qualified_table_name)
}
Expression::InList { expr, values, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)?;
for item in values {
validate_expression_columns(item, table_schema, qualified_table_name)?;
}
Ok(())
}
Expression::Like { expr, pattern, .. } => {
validate_expression_columns(expr, table_schema, qualified_table_name)?;
validate_expression_columns(pattern, table_schema, qualified_table_name)
}
Expression::Literal(_)
| Expression::Wildcard
| Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_) => Ok(()),
_ => Ok(()),
}
}