AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/sqlexec/src/exec.rs

Latest change fa37e2544d31a1199954a395f9e5b3ebb237b17f - feat(sql): aggregates (COUNT/SUM/AVG/MIN/MAX) + GROUP BY (gpt-5.5) by Ólafur Búi Ólafsson

//! The top-level [`Executor`]: owns a [`BTree`] and runs SQL text against it.

use std::path::Path;

use akurai_sql::{
    parse, AggregateArg, AggregateExpr, AggregateFunction, CreateTableStmt, DataType, DeleteStmt,
    Direction, InsertStmt, Literal, SelectColumn, SelectStmt, Statement, UpdateStmt,
};
use akurai_storage::BTree;

use crate::catalog::Schema;
use crate::error::ExecError;
use crate::row::{decode_row, encode_row};
use crate::value::Value;
use crate::{eval, ResultSet};

/// What a successful statement produced.
#[derive(Debug, PartialEq)]
pub enum Outcome {
    /// A DDL statement (`CREATE TABLE`) succeeded.
    Created(String),
    /// A DML statement wrote rows; carries the count affected.
    Affected(usize),
    /// A query produced a result set.
    Rows(ResultSet),
}

/// A SQL engine over a single B+tree file.
pub struct Executor {
    tree: BTree,
}

impl Executor {
    /// Open (or create) the database file backing this engine.
    pub fn open(path: impl AsRef<Path>) -> Result<Executor, ExecError> {
        Ok(Executor {
            tree: BTree::open(path)?,
        })
    }

    /// Parse and run one SQL statement.
    pub fn execute(&mut self, sql: &str) -> Result<Outcome, ExecError> {
        match parse(sql)? {
            Statement::CreateTable(stmt) => self.create_table(stmt),
            Statement::Insert(stmt) => self.insert(stmt),
            Statement::Update(stmt) => self.update(stmt),
            Statement::Delete(stmt) => self.delete(stmt),
            Statement::Select(stmt) => self.select(stmt),
        }
    }

    // ---- DDL --------------------------------------------------------------

    fn create_table(&mut self, stmt: CreateTableStmt) -> Result<Outcome, ExecError> {
        let key = Schema::catalog_key(&stmt.table);
        if self.tree.get(&key)?.is_some() {
            return Err(ExecError::TableExists(stmt.table));
        }
        let schema = Schema::from_defs(&stmt.table, &stmt.columns);
        self.tree.insert(&key, &schema.encode())?;
        // Seed the rowid counter at 0 so INSERT can read it unconditionally.
        self.tree
            .insert(&Schema::seq_key(&stmt.table), &0u64.to_be_bytes())?;
        self.tree.commit()?;
        Ok(Outcome::Created(stmt.table))
    }

    // ---- INSERT -----------------------------------------------------------

    fn insert(&mut self, stmt: InsertStmt) -> Result<Outcome, ExecError> {
        let schema = self.load_schema(&stmt.table)?;

        if stmt.columns.len() != stmt.values.len() {
            return Err(ExecError::ColumnCountMismatch {
                expected: stmt.columns.len(),
                got: stmt.values.len(),
            });
        }

        // Place each supplied value into its schema slot; unmentioned columns
        // default to NULL.
        let mut cells: Vec<Value> = vec![Value::Null; schema.columns.len()];
        for (name, lit) in stmt.columns.iter().zip(stmt.values.iter()) {
            let idx = schema
                .index_of(name)
                .ok_or_else(|| ExecError::UnknownColumn(name.clone()))?;
            cells[idx] = coerce(lit, &schema.columns[idx])?;
        }

        // Enforce NOT NULL after placement.
        for (i, col) in schema.columns.iter().enumerate() {
            if col.not_null && cells[i].is_null() {
                return Err(ExecError::NotNull(col.name.clone()));
            }
        }

        let rowid = self.next_rowid(&stmt.table)?;
        let key = data_key(&stmt.table, rowid);
        self.tree.insert(&key, &encode_row(&cells))?;
        self.tree.commit()?;
        Ok(Outcome::Affected(1))
    }

    // ---- UPDATE -----------------------------------------------------------

    fn update(&mut self, stmt: UpdateStmt) -> Result<Outcome, ExecError> {
        let schema = self.load_schema(&stmt.table)?;

        // Resolve every assignment up front: validates each target column exists
        // and that its value coerces to the declared type before touching rows.
        let mut sets: Vec<(usize, Value)> = Vec::with_capacity(stmt.assignments.len());
        for (name, lit) in &stmt.assignments {
            let idx = schema
                .index_of(name)
                .ok_or_else(|| ExecError::UnknownColumn(name.clone()))?;
            let value = coerce(lit, &schema.columns[idx])?;
            sets.push((idx, value));
        }

        // Scan, filter by WHERE, and stage the rewritten rows (same key/rowid).
        let (start, end) = data_bounds(&stmt.table);
        let mut rewrites: Vec<(Vec<u8>, Vec<Value>)> = Vec::new();
        for (key, bytes) in self.tree.range(&start, &end)? {
            let mut row = decode_row(&bytes)?;
            if !matches_where(&stmt.where_clause, &schema, &row)? {
                continue;
            }
            for (idx, value) in &sets {
                row[*idx] = value.clone();
            }
            // Re-enforce NOT NULL after applying the assignments.
            for (i, col) in schema.columns.iter().enumerate() {
                if col.not_null && row[i].is_null() {
                    return Err(ExecError::NotNull(col.name.clone()));
                }
            }
            rewrites.push((key, row));
        }

        let n = rewrites.len();
        for (key, row) in rewrites {
            self.tree.insert(&key, &encode_row(&row))?;
        }
        self.tree.commit()?;
        Ok(Outcome::Affected(n))
    }

    // ---- DELETE -----------------------------------------------------------

    fn delete(&mut self, stmt: DeleteStmt) -> Result<Outcome, ExecError> {
        let schema = self.load_schema(&stmt.table)?;

        let (start, end) = data_bounds(&stmt.table);
        let mut victims: Vec<Vec<u8>> = Vec::new();
        for (key, bytes) in self.tree.range(&start, &end)? {
            let row = decode_row(&bytes)?;
            if matches_where(&stmt.where_clause, &schema, &row)? {
                victims.push(key);
            }
        }

        let n = victims.len();
        for key in victims {
            self.tree.delete(&key)?;
        }
        self.tree.commit()?;
        Ok(Outcome::Affected(n))
    }

    // ---- SELECT -----------------------------------------------------------

    fn select(&mut self, stmt: SelectStmt) -> Result<Outcome, ExecError> {
        let schema = self.load_schema(&stmt.table)?;

        // 1. Scan the table's data range and decode rows.
        let (start, end) = data_bounds(&stmt.table);
        let mut rows: Vec<Vec<Value>> = Vec::new();
        for (_k, v) in self.tree.range(&start, &end)? {
            rows.push(decode_row(&v)?);
        }

        // 2. WHERE filter.
        if let Some(pred) = &stmt.where_clause {
            let mut kept = Vec::with_capacity(rows.len());
            for row in rows {
                if eval::eval_predicate(pred, &schema, &row)? {
                    kept.push(row);
                }
            }
            rows = kept;
        }

        if is_aggregate_query(&stmt) {
            return aggregate_select(&stmt, &schema, rows).map(Outcome::Rows);
        }

        // 3. ORDER BY.
        if let Some(order) = &stmt.order_by {
            let idx = schema
                .index_of(&order.column)
                .ok_or_else(|| ExecError::UnknownColumn(order.column.clone()))?;
            rows.sort_by(|a, b| {
                let ord = a[idx].order_key(&b[idx]);
                match order.direction {
                    Direction::Asc => ord,
                    Direction::Desc => ord.reverse(),
                }
            });
        }

        // 4. LIMIT.
        if let Some(limit) = stmt.limit {
            let n = limit.max(0) as usize;
            rows.truncate(n);
        }

        // 5. Projection.
        let proj = projection(&stmt.columns, &schema)?;
        let columns: Vec<String> = proj
            .iter()
            .map(|i| schema.columns[*i].name.clone())
            .collect();
        let out_rows: Vec<Vec<Value>> = rows
            .into_iter()
            .map(|row| proj.iter().map(|i| row[*i].clone()).collect())
            .collect();

        Ok(Outcome::Rows(ResultSet {
            columns,
            rows: out_rows,
        }))
    }

    // ---- helpers ----------------------------------------------------------

    fn load_schema(&mut self, table: &str) -> Result<Schema, ExecError> {
        let bytes = self
            .tree
            .get(&Schema::catalog_key(table))?
            .ok_or_else(|| ExecError::NoSuchTable(table.to_string()))?;
        Schema::decode(table, &bytes)
    }

    /// Read, bump, and persist the per-table rowid counter; returns the rowid
    /// to use for the new row (1-based).
    fn next_rowid(&mut self, table: &str) -> Result<u64, ExecError> {
        let key = Schema::seq_key(table);
        let current = match self.tree.get(&key)? {
            Some(b) if b.len() == 8 => {
                let mut a = [0u8; 8];
                a.copy_from_slice(&b);
                u64::from_be_bytes(a)
            }
            Some(_) => return Err(ExecError::Corrupt("rowid counter wrong size")),
            None => 0,
        };
        let next = current + 1;
        self.tree.insert(&key, &next.to_be_bytes())?;
        Ok(next)
    }
}

/// Whether `row` satisfies an optional `WHERE` predicate. A missing predicate
/// matches every row (the bare `UPDATE`/`DELETE` whole-table case).
fn matches_where(
    where_clause: &Option<akurai_sql::Expr>,
    schema: &Schema,
    row: &[Value],
) -> Result<bool, ExecError> {
    match where_clause {
        Some(pred) => eval::eval_predicate(pred, schema, row),
        None => Ok(true),
    }
}

/// Coerce a parser literal into the column's declared type, or reject it.
fn coerce(lit: &Literal, col: &crate::catalog::Column) -> Result<Value, ExecError> {
    match (lit, col.ty) {
        (Literal::Null, _) => Ok(Value::Null),
        (Literal::Int(i), DataType::Integer) => Ok(Value::Int(*i)),
        // Widen an integer literal into a REAL column.
        (Literal::Int(i), DataType::Real) => Ok(Value::Float(*i as f64)),
        (Literal::Float(f), DataType::Real) => Ok(Value::Float(*f)),
        (Literal::Str(s), DataType::Text) => Ok(Value::Str(s.clone())),
        _ => Err(ExecError::TypeMismatch {
            column: col.name.clone(),
        }),
    }
}

/// Resolve the projection list into schema column indices.
fn projection(cols: &[SelectColumn], schema: &Schema) -> Result<Vec<usize>, ExecError> {
    // `SELECT *` (or a list containing `*`) projects every column in order.
    if cols.iter().any(|c| matches!(c, SelectColumn::All)) {
        return Ok((0..schema.columns.len()).collect());
    }
    cols.iter()
        .map(|c| match c {
            SelectColumn::All => unreachable!(),
            SelectColumn::Named(name) => schema
                .index_of(name)
                .ok_or_else(|| ExecError::UnknownColumn(name.clone())),
            SelectColumn::Aggregate(_) => Err(ExecError::Unsupported(
                "aggregate projection in non-aggregate SELECT",
            )),
        })
        .collect()
}

fn is_aggregate_query(stmt: &SelectStmt) -> bool {
    stmt.group_by.is_some()
        || stmt
            .columns
            .iter()
            .any(|c| matches!(c, SelectColumn::Aggregate(_)))
}

fn aggregate_select(
    stmt: &SelectStmt,
    schema: &Schema,
    rows: Vec<Vec<Value>>,
) -> Result<ResultSet, ExecError> {
    let plan = AggregatePlan::new(stmt, schema)?;
    let mut groups = build_groups(&plan, rows)?;

    if let Some(order) = &stmt.order_by {
        if plan.group_name.as_deref() == Some(order.column.as_str()) {
            groups.sort_by(|a, b| {
                let ord = match (&a.key, &b.key) {
                    (Some(a), Some(b)) => a.order_key(b),
                    _ => std::cmp::Ordering::Equal,
                };
                match order.direction {
                    Direction::Asc => ord,
                    Direction::Desc => ord.reverse(),
                }
            });
        }
    }

    let mut out_rows: Vec<Vec<Value>> = groups
        .iter()
        .map(|group| plan.materialize_row(group))
        .collect::<Result<_, _>>()?;
    let columns = plan.output_columns();

    if let Some(order) = &stmt.order_by {
        if plan.group_name.as_deref() != Some(order.column.as_str()) {
            if let Some(idx) = columns.iter().position(|c| c == &order.column) {
                out_rows.sort_by(|a, b| {
                    let ord = a[idx].order_key(&b[idx]);
                    match order.direction {
                        Direction::Asc => ord,
                        Direction::Desc => ord.reverse(),
                    }
                });
            } else if schema.index_of(&order.column).is_none() {
                return Err(ExecError::UnknownColumn(order.column.clone()));
            } else if stmt.group_by.is_some() {
                return Err(ExecError::Unsupported(
                    "ORDER BY in grouped SELECT must reference the group column or output",
                ));
            }
        }
    }

    if let Some(limit) = stmt.limit {
        let n = limit.max(0) as usize;
        out_rows.truncate(n);
    }

    Ok(ResultSet {
        columns,
        rows: out_rows,
    })
}

struct AggregatePlan {
    group_name: Option<String>,
    group_idx: Option<usize>,
    outputs: Vec<AggregateOutput>,
    aggregates: Vec<PlannedAggregate>,
}

enum AggregateOutput {
    GroupColumn(String),
    Aggregate(usize),
}

struct PlannedAggregate {
    expr: AggregateExpr,
    arg_idx: Option<usize>,
}

impl AggregatePlan {
    fn new(stmt: &SelectStmt, schema: &Schema) -> Result<AggregatePlan, ExecError> {
        let group_idx = match &stmt.group_by {
            Some(name) => Some(
                schema
                    .index_of(name)
                    .ok_or_else(|| ExecError::UnknownColumn(name.clone()))?,
            ),
            None => None,
        };

        let mut outputs = Vec::with_capacity(stmt.columns.len());
        let mut aggregates = Vec::new();
        for column in &stmt.columns {
            match column {
                SelectColumn::All => {
                    return Err(ExecError::Unsupported(
                        "SELECT * cannot be mixed with aggregates or GROUP BY",
                    ))
                }
                SelectColumn::Named(name) => {
                    if stmt.group_by.as_ref() == Some(name) {
                        outputs.push(AggregateOutput::GroupColumn(name.clone()));
                    } else if stmt.group_by.is_some() {
                        return Err(ExecError::Unsupported(
                            "grouped SELECT can only project the group column and aggregates",
                        ));
                    } else {
                        return Err(ExecError::Unsupported(
                            "aggregate SELECT can only project aggregates without GROUP BY",
                        ));
                    }
                }
                SelectColumn::Aggregate(expr) => {
                    let idx = aggregates.len();
                    aggregates.push(plan_aggregate(expr, schema)?);
                    outputs.push(AggregateOutput::Aggregate(idx));
                }
            }
        }

        Ok(AggregatePlan {
            group_name: stmt.group_by.clone(),
            group_idx,
            outputs,
            aggregates,
        })
    }

    fn output_columns(&self) -> Vec<String> {
        self.outputs
            .iter()
            .map(|output| match output {
                AggregateOutput::GroupColumn(name) => name.clone(),
                AggregateOutput::Aggregate(idx) => aggregate_label(&self.aggregates[*idx].expr),
            })
            .collect()
    }

    fn materialize_row(&self, group: &GroupState) -> Result<Vec<Value>, ExecError> {
        self.outputs
            .iter()
            .map(|output| match output {
                AggregateOutput::GroupColumn(_) => Ok(group.key.clone().unwrap_or(Value::Null)),
                AggregateOutput::Aggregate(idx) => Ok(group.states[*idx].finish()),
            })
            .collect()
    }
}

fn plan_aggregate(expr: &AggregateExpr, schema: &Schema) -> Result<PlannedAggregate, ExecError> {
    let arg_idx = match &expr.arg {
        AggregateArg::All => {
            if expr.func != AggregateFunction::Count {
                return Err(ExecError::Unsupported("only COUNT accepts '*'"));
            }
            None
        }
        AggregateArg::Column(name) => {
            let idx = schema
                .index_of(name)
                .ok_or_else(|| ExecError::UnknownColumn(name.clone()))?;
            if matches!(expr.func, AggregateFunction::Sum | AggregateFunction::Avg)
                && schema.columns[idx].ty == DataType::Text
            {
                return Err(ExecError::TypeMismatch {
                    column: name.clone(),
                });
            }
            Some(idx)
        }
    };
    Ok(PlannedAggregate {
        expr: expr.clone(),
        arg_idx,
    })
}

struct GroupState {
    key: Option<Value>,
    states: Vec<AggregateState>,
}

fn build_groups(plan: &AggregatePlan, rows: Vec<Vec<Value>>) -> Result<Vec<GroupState>, ExecError> {
    let mut groups: Vec<GroupState> = Vec::new();

    if let Some(group_idx) = plan.group_idx {
        for row in rows {
            let key = row.get(group_idx).cloned().unwrap_or(Value::Null);
            let idx = match groups
                .iter()
                .position(|group| group.key.as_ref() == Some(&key))
            {
                Some(idx) => idx,
                None => {
                    groups.push(GroupState {
                        key: Some(key),
                        states: new_aggregate_states(plan),
                    });
                    groups.len() - 1
                }
            };
            update_group(plan, &mut groups[idx], &row)?;
        }
    } else {
        groups.push(GroupState {
            key: None,
            states: new_aggregate_states(plan),
        });
        for row in rows {
            update_group(plan, &mut groups[0], &row)?;
        }
    }

    Ok(groups)
}

fn new_aggregate_states(plan: &AggregatePlan) -> Vec<AggregateState> {
    plan.aggregates
        .iter()
        .map(|agg| AggregateState::new(agg.expr.func))
        .collect()
}

fn update_group(
    plan: &AggregatePlan,
    group: &mut GroupState,
    row: &[Value],
) -> Result<(), ExecError> {
    for (state, agg) in group.states.iter_mut().zip(plan.aggregates.iter()) {
        let value = agg.arg_idx.and_then(|idx| row.get(idx));
        state.update(value)?;
    }
    Ok(())
}

#[derive(Clone)]
struct AggregateState {
    func: AggregateFunction,
    count: i64,
    numeric_count: i64,
    int_sum: i64,
    float_sum: f64,
    saw_float: bool,
    int_overflow: bool,
    min: Option<Value>,
    max: Option<Value>,
}

impl AggregateState {
    fn new(func: AggregateFunction) -> AggregateState {
        AggregateState {
            func,
            count: 0,
            numeric_count: 0,
            int_sum: 0,
            float_sum: 0.0,
            saw_float: false,
            int_overflow: false,
            min: None,
            max: None,
        }
    }

    fn update(&mut self, value: Option<&Value>) -> Result<(), ExecError> {
        match self.func {
            AggregateFunction::Count => {
                if match value {
                    Some(v) => !v.is_null(),
                    None => true,
                } {
                    self.count = self.count.saturating_add(1);
                }
            }
            AggregateFunction::Sum | AggregateFunction::Avg => {
                let Some(value) = value else {
                    return Ok(());
                };
                match value {
                    Value::Null => {}
                    Value::Int(i) => self.add_int(*i),
                    Value::Float(f) => self.add_float(*f),
                    Value::Str(_) => {
                        return Err(ExecError::TypeMismatch {
                            column: aggregate_label_func(self.func).to_string(),
                        })
                    }
                }
            }
            AggregateFunction::Min => {
                if let Some(value) = value.filter(|v| !v.is_null()) {
                    let replace = match &self.min {
                        Some(current) => value.order_key(current).is_lt(),
                        None => true,
                    };
                    if replace {
                        self.min = Some(value.clone());
                    }
                }
            }
            AggregateFunction::Max => {
                if let Some(value) = value.filter(|v| !v.is_null()) {
                    let replace = match &self.max {
                        Some(current) => value.order_key(current).is_gt(),
                        None => true,
                    };
                    if replace {
                        self.max = Some(value.clone());
                    }
                }
            }
        }
        Ok(())
    }

    fn add_int(&mut self, value: i64) {
        self.numeric_count = self.numeric_count.saturating_add(1);
        self.float_sum += value as f64;
        match self.int_sum.checked_add(value) {
            Some(sum) => self.int_sum = sum,
            None => self.int_overflow = true,
        }
    }

    fn add_float(&mut self, value: f64) {
        self.numeric_count = self.numeric_count.saturating_add(1);
        self.float_sum += value;
        self.saw_float = true;
    }

    fn finish(&self) -> Value {
        match self.func {
            AggregateFunction::Count => Value::Int(self.count),
            AggregateFunction::Sum => {
                if self.numeric_count == 0 {
                    Value::Null
                } else if self.saw_float || self.int_overflow {
                    Value::Float(self.float_sum)
                } else {
                    Value::Int(self.int_sum)
                }
            }
            AggregateFunction::Avg => {
                if self.numeric_count == 0 {
                    Value::Null
                } else {
                    Value::Float(self.float_sum / self.numeric_count as f64)
                }
            }
            AggregateFunction::Min => self.min.clone().unwrap_or(Value::Null),
            AggregateFunction::Max => self.max.clone().unwrap_or(Value::Null),
        }
    }
}

fn aggregate_label(expr: &AggregateExpr) -> String {
    let arg = match &expr.arg {
        AggregateArg::All => "*".to_string(),
        AggregateArg::Column(name) => name.clone(),
    };
    format!("{}({arg})", aggregate_label_func(expr.func))
}

fn aggregate_label_func(func: AggregateFunction) -> &'static str {
    match func {
        AggregateFunction::Count => "COUNT",
        AggregateFunction::Sum => "SUM",
        AggregateFunction::Avg => "AVG",
        AggregateFunction::Min => "MIN",
        AggregateFunction::Max => "MAX",
    }
}

/// The data key for `table`/`rowid`: `<table>:<rowid_be>`.
fn data_key(table: &str, rowid: u64) -> Vec<u8> {
    let mut k = Vec::with_capacity(table.len() + 9);
    k.extend_from_slice(table.as_bytes());
    k.push(b':');
    k.extend_from_slice(&rowid.to_be_bytes());
    k
}

/// The half-open `[start, end)` key window covering every row of `table`.
fn data_bounds(table: &str) -> (Vec<u8>, Vec<u8>) {
    let mut prefix = Vec::with_capacity(table.len() + 1);
    prefix.extend_from_slice(table.as_bytes());
    prefix.push(b':');
    let end = prefix_upper(&prefix);
    (prefix, end)
}

/// The smallest key strictly greater than every key beginning with `prefix`.
fn prefix_upper(prefix: &[u8]) -> Vec<u8> {
    let mut end = prefix.to_vec();
    while let Some(last) = end.last_mut() {
        if *last != 0xFF {
            *last += 1;
            return end;
        }
        end.pop();
    }
    // All-0xFF prefix: no upper bound shorter than infinity. Fall back to a
    // sentinel that the storage layer treats as "scan to the end".
    vec![0xFF; prefix.len() + 1]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prefix_upper_increments_last_byte() {
        assert_eq!(prefix_upper(b"users:"), b"users;".to_vec());
    }

    #[test]
    fn data_key_orders_by_rowid() {
        assert!(data_key("t", 1) < data_key("t", 2));
        assert!(data_key("t", 2) < data_key("t", 256));
    }
}