lix 0.12.1

Embeddable version control for apps and AI agents.
Documentation
use datafusion::sql::parser::Statement as DataFusionStatement;
use datafusion::sql::sqlparser::ast::{Query, SetExpr, Statement as SqlStatement};

use crate::LixError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SqlStatementKind {
    Read,
    Write,
    Other,
}

pub(crate) fn validate_supported_datafusion_statement_ast(
    statement: &DataFusionStatement,
) -> Result<(), LixError> {
    match statement {
        DataFusionStatement::Statement(statement) => validate_supported_ast_statement(statement),
        DataFusionStatement::Explain(explain) => {
            if classify_datafusion_statement(explain.statement.as_ref()) == SqlStatementKind::Write
            {
                return Err(unsupported_sql_error(
                    "EXPLAIN of write statements is not supported by Lix SQL",
                ));
            }
            validate_supported_datafusion_statement_ast(explain.statement.as_ref())
        }
        _ => Err(unsupported_sql_error(format!(
            "SQL statement is not supported by Lix SQL: {statement}"
        ))),
    }
}

pub(crate) fn classify_datafusion_statement(statement: &DataFusionStatement) -> SqlStatementKind {
    match statement {
        DataFusionStatement::Statement(statement) => classify_ast_statement(statement),
        DataFusionStatement::Explain(explain) => {
            classify_datafusion_statement(explain.statement.as_ref())
        }
        _ => SqlStatementKind::Other,
    }
}

fn classify_ast_statement(statement: &SqlStatement) -> SqlStatementKind {
    match statement {
        SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => {
            SqlStatementKind::Write
        }
        SqlStatement::Query(_) | SqlStatement::Explain { .. } => SqlStatementKind::Read,
        _ => SqlStatementKind::Other,
    }
}

fn validate_supported_ast_statement(statement: &SqlStatement) -> Result<(), LixError> {
    match statement {
        SqlStatement::Query(query) => validate_supported_query(query),
        SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
        SqlStatement::Explain { statement, .. } => {
            if classify_ast_statement(statement.as_ref()) == SqlStatementKind::Write {
                return Err(unsupported_sql_error(
                    "EXPLAIN of write statements is not supported by Lix SQL",
                ));
            }
            validate_supported_ast_statement(statement)
        }
        _ => Err(unsupported_sql_error(format!(
            "SQL statement is not supported by Lix SQL: {statement}"
        ))),
    }
}

fn validate_supported_query(query: &Query) -> Result<(), LixError> {
    if query.with.as_ref().is_some_and(|with| with.recursive) {
        return Err(
            unsupported_sql_error("recursive CTEs are not supported by Lix SQL").with_hint(
                "Use explicit commit graph surfaces such as lix_commit and lix_commit_edge, or a typed <schema>_history surface, instead of WITH RECURSIVE.",
            ),
        );
    }

    if let Some(with) = &query.with {
        for cte in &with.cte_tables {
            validate_supported_query(&cte.query)?;
        }
    }
    validate_supported_set_expr(&query.body)
}

fn validate_supported_set_expr(expr: &SetExpr) -> Result<(), LixError> {
    match expr {
        SetExpr::Query(query) => validate_supported_query(query),
        SetExpr::SetOperation { left, right, .. } => {
            validate_supported_set_expr(left)?;
            validate_supported_set_expr(right)
        }
        _ => Ok(()),
    }
}

fn unsupported_sql_error(message: impl Into<String>) -> LixError {
    LixError::new(LixError::CODE_UNSUPPORTED_SQL, message)
}