use std::collections::HashSet;
use vibesql_ast::{Expression, SelectItem, SelectStmt};
use vibesql_storage::Database;
pub fn can_eliminate_distinct(stmt: &SelectStmt, database: &Database) -> bool {
let table_name = match &stmt.from {
Some(vibesql_ast::FromClause::Table { name, .. }) => name.as_str(),
_ => return false,
};
let Some(table) = database.get_table(table_name) else {
return false;
};
let schema = &table.schema;
let select_columns = match extract_select_columns(&stmt.select_list, schema) {
Some(cols) => cols,
None => return false, };
if select_columns.is_empty() {
return false;
}
if select_columns.iter().any(|c| c.eq_ignore_ascii_case("rowid")) {
return true;
}
if let Some(rowid_idx) = schema.rowid_alias_column {
let rowid_col = &schema.columns[rowid_idx].name;
if select_columns.iter().any(|c| c.eq_ignore_ascii_case(rowid_col)) {
return true;
}
}
if let Some(ref pk_cols) = schema.primary_key {
if covers_columns(pk_cols, &select_columns, &stmt.where_clause, schema) {
if all_columns_not_null(pk_cols, schema) {
return true;
}
}
}
for unique_cols in &schema.unique_constraints {
if covers_columns(unique_cols, &select_columns, &stmt.where_clause, schema)
&& all_columns_not_null(unique_cols, schema)
{
return true;
}
}
for index_name in database.list_indexes() {
let Some(index) = database.get_index(&index_name) else {
continue;
};
if !index.table_name.eq_ignore_ascii_case(table_name) || !index.unique {
continue;
}
if database.catalog.find_index_by_name(&index_name).map(|m| m.is_partial()).unwrap_or(false)
{
continue;
}
let index_cols: Vec<String> =
index.columns.iter().filter_map(|c| c.column_name().map(|s| s.to_string())).collect();
if index_cols.len() != index.columns.len() {
continue;
}
if covers_columns(&index_cols, &select_columns, &stmt.where_clause, schema)
&& all_columns_not_null(&index_cols, schema)
{
return true;
}
}
false
}
fn extract_select_columns(
select_list: &[SelectItem],
schema: &vibesql_catalog::TableSchema,
) -> Option<Vec<String>> {
let mut columns = Vec::new();
for item in select_list {
match item {
SelectItem::Wildcard { .. } => {
for col in &schema.columns {
columns.push(col.name.clone());
}
}
SelectItem::QualifiedWildcard { .. } => {
for col in &schema.columns {
columns.push(col.name.clone());
}
}
SelectItem::Expression { expr, .. } => {
if let Some(col_name) = extract_column_from_expr(expr) {
columns.push(col_name);
} else {
return None;
}
}
}
}
Some(columns)
}
fn extract_column_from_expr(expr: &Expression) -> Option<String> {
match expr {
Expression::ColumnRef(col_id) => Some(col_id.column_canonical().to_string()),
_ => None,
}
}
fn covers_columns(
unique_cols: &[String],
select_cols: &[String],
where_clause: &Option<Expression>,
schema: &vibesql_catalog::TableSchema,
) -> bool {
let select_set: HashSet<String> = select_cols.iter().map(|c| c.to_lowercase()).collect();
let fixed_cols = get_fixed_columns(where_clause, schema);
for unique_col in unique_cols {
let unique_col_lower = unique_col.to_lowercase();
if !select_set.contains(&unique_col_lower) && !fixed_cols.contains(&unique_col_lower) {
return false;
}
}
true
}
fn get_fixed_columns(
where_clause: &Option<Expression>,
_schema: &vibesql_catalog::TableSchema,
) -> HashSet<String> {
let mut fixed = HashSet::new();
if let Some(expr) = where_clause {
collect_fixed_columns(expr, &mut fixed);
}
fixed
}
fn collect_fixed_columns(expr: &Expression, fixed: &mut HashSet<String>) {
use vibesql_ast::BinaryOperator;
match expr {
Expression::BinaryOp { op: BinaryOperator::Equal, left, right } => {
if let Expression::ColumnRef(col_id) = left.as_ref() {
if is_constant(right) {
fixed.insert(col_id.column_canonical().to_lowercase());
}
}
if let Expression::ColumnRef(col_id) = right.as_ref() {
if is_constant(left) {
fixed.insert(col_id.column_canonical().to_lowercase());
}
}
}
Expression::Conjunction(exprs) => {
for e in exprs {
collect_fixed_columns(e, fixed);
}
}
Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
collect_fixed_columns(left, fixed);
collect_fixed_columns(right, fixed);
}
_ => {}
}
}
fn is_constant(expr: &Expression) -> bool {
matches!(
expr,
Expression::Literal(_)
| Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_)
)
}
fn all_columns_not_null(columns: &[String], schema: &vibesql_catalog::TableSchema) -> bool {
for col_name in columns {
let col_lower = col_name.to_lowercase();
let col = schema.columns.iter().find(|c| c.name.to_lowercase() == col_lower);
match col {
Some(c) => {
if c.nullable {
return false;
}
}
None => {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use vibesql_ast::{ColumnIdentifier, OrderDirection, SelectStmt};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Database;
use vibesql_types::DataType;
use super::*;
fn create_test_database() -> Database {
let mut db = Database::new();
let t1_cols = vec![
ColumnSchema::new("a".to_string(), DataType::Integer, true),
ColumnSchema::new("b".to_string(), DataType::Integer, true),
ColumnSchema::new("c".to_string(), DataType::Integer, true),
ColumnSchema::new("d".to_string(), DataType::Integer, true),
];
let t1_schema = TableSchema::new("t1".to_string(), t1_cols);
db.create_table(t1_schema).unwrap();
let t4_cols = vec![
ColumnSchema::new("a".to_string(), DataType::Integer, true), ColumnSchema::new("b".to_string(), DataType::Integer, false), ColumnSchema::new("c".to_string(), DataType::Integer, false), ColumnSchema::new("d".to_string(), DataType::Integer, false), ];
let t4_schema = TableSchema::new("t4".to_string(), t4_cols);
db.create_table(t4_schema).unwrap();
db.create_index(
"i1".to_string(),
"t1".to_string(),
true, vec![
vibesql_ast::IndexColumn::new_column("b".to_string(), OrderDirection::Asc),
vibesql_ast::IndexColumn::new_column("c".to_string(), OrderDirection::Asc),
],
)
.unwrap();
db.create_index(
"t4i1".to_string(),
"t4".to_string(),
true, vec![
vibesql_ast::IndexColumn::new_column("b".to_string(), OrderDirection::Asc),
vibesql_ast::IndexColumn::new_column("c".to_string(), OrderDirection::Asc),
],
)
.unwrap();
db
}
fn make_select_stmt(table: &str, columns: &[&str]) -> SelectStmt {
let select_list: Vec<SelectItem> = columns
.iter()
.map(|c| SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple(c, false)),
alias: None,
source_text: None,
})
.collect();
SelectStmt {
with_clause: None,
distinct: true,
select_list,
into_table: None,
into_variables: None,
from: Some(vibesql_ast::FromClause::Table {
index_hint: None,
name: table.to_string(),
alias: None,
column_aliases: None,
quoted: false,
}),
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
}
}
#[test]
fn test_distinct_not_eliminated_nullable_columns() {
let db = create_test_database();
let stmt = make_select_stmt("t1", &["b", "c"]);
assert!(!can_eliminate_distinct(&stmt, &db));
}
#[test]
fn test_distinct_eliminated_not_null_columns() {
let db = create_test_database();
let stmt = make_select_stmt("t4", &["b", "c"]);
assert!(can_eliminate_distinct(&stmt, &db));
}
#[test]
fn test_distinct_not_eliminated_partial_unique() {
let db = create_test_database();
let stmt = make_select_stmt("t4", &["a", "b"]);
assert!(!can_eliminate_distinct(&stmt, &db));
}
}