alopex-sql 0.7.2

SQL parser components for the Alopex DB dialect
Documentation
use alopex_core::kv::memory::MemoryKV;
use alopex_sql::ast::ddl::{ColumnDef, CreateTable, DataType};
use alopex_sql::ast::dml::{FromItem, JoinType, LITERAL_TABLE, Select, SelectItem};
use alopex_sql::ast::expr::BinaryOp;
use alopex_sql::catalog::persistent::TableFqn;
use alopex_sql::catalog::{
    Catalog, CatalogOverlay, ColumnMetadata, MemoryCatalog, PersistentCatalog, TableMetadata,
    TxnCatalogView,
};
use alopex_sql::planner::{
    LogicalPlan, PlanningDiagnosticSeverity, ProjectedColumn, Projection, TableReferenceAccess,
    TableReferenceExtractor, TableReferenceSource, TypedExpr, TypedExprKind, plan_sql_for_routing,
    plan_statement_for_routing,
};
use alopex_sql::{ResolvedType, Span, Statement, StatementKind};

fn table(name: &str) -> TableMetadata {
    TableMetadata::new(name, vec![ColumnMetadata::new("id", ResolvedType::Integer)])
}

fn select_from(name: &str) -> Statement {
    Statement {
        kind: StatementKind::Select(Select {
            distinct: false,
            projection: vec![SelectItem::Wildcard {
                span: Span::empty(),
            }],
            from: vec![FromItem::Table {
                name: name.to_string(),
                alias: None,
                span: Span::empty(),
            }],
            selection: None,
            group_by: None,
            having: None,
            order_by: Vec::new(),
            limit: None,
            offset: None,
            span: Span::empty(),
        }),
        span: Span::empty(),
    }
}

#[test]
fn plan_statement_for_routing_returns_plan_kind_and_table_reference() {
    let mut catalog = MemoryCatalog::new();
    catalog.create_table(table("users")).unwrap();

    let planned = plan_statement_for_routing(&catalog, &select_from("users")).unwrap();

    assert_eq!(planned.plan.operation_name(), "SELECT");
    assert!(matches!(planned.statement_kind(), StatementKind::Select(_)));
    assert!(planned.diagnostics().is_empty());
    assert_eq!(planned.table_references().len(), 1);
    assert_eq!(planned.table_references()[0].table_name, "users");
    assert_eq!(
        planned.table_references()[0].access,
        TableReferenceAccess::Read
    );
    assert_eq!(
        planned.table_references()[0].source,
        TableReferenceSource::LogicalPlanScan
    );
}

#[test]
fn plan_statement_for_routing_marks_create_table_access() {
    let catalog = MemoryCatalog::new();
    let stmt = Statement {
        kind: StatementKind::CreateTable(CreateTable {
            if_not_exists: false,
            name: "events".to_string(),
            columns: vec![ColumnDef {
                name: "id".to_string(),
                data_type: DataType::Integer,
                constraints: Vec::new(),
                span: Span::empty(),
            }],
            constraints: Vec::new(),
            with_options: Vec::new(),
            span: Span::empty(),
        }),
        span: Span::empty(),
    };

    let planned = plan_statement_for_routing(&catalog, &stmt).unwrap();

    assert_eq!(planned.plan.operation_name(), "CREATE TABLE");
    assert!(matches!(
        planned.statement_kind(),
        StatementKind::CreateTable(_)
    ));
    assert_eq!(planned.table_references().len(), 1);
    assert_eq!(planned.table_references()[0].table_name, "events");
    assert_eq!(
        planned.table_references()[0].access,
        TableReferenceAccess::Create
    );
}

#[test]
fn join_plan_extracts_both_table_references() {
    let mut catalog = MemoryCatalog::new();
    catalog.create_table(table("users")).unwrap();
    catalog.create_table(table("orders")).unwrap();

    let stmt = Statement {
        kind: StatementKind::Select(Select {
            distinct: false,
            projection: vec![SelectItem::Wildcard {
                span: Span::empty(),
            }],
            from: vec![FromItem::Join {
                left: Box::new(FromItem::Table {
                    name: "users".to_string(),
                    alias: None,
                    span: Span::empty(),
                }),
                right: Box::new(FromItem::Table {
                    name: "orders".to_string(),
                    alias: None,
                    span: Span::empty(),
                }),
                join_type: JoinType::Inner,
                condition: None,
                using: None,
                span: Span::empty(),
            }],
            selection: None,
            group_by: None,
            having: None,
            order_by: Vec::new(),
            limit: None,
            offset: None,
            span: Span::empty(),
        }),
        span: Span::empty(),
    };

    let planned = plan_statement_for_routing(&catalog, &stmt).unwrap();

    assert!(planned.diagnostics().is_empty());
    assert_eq!(planned.table_references().len(), 2);
    assert!(
        planned
            .table_references()
            .iter()
            .any(|reference| reference.table_name == "users"
                && reference.access == TableReferenceAccess::Read
                && reference.source == TableReferenceSource::LogicalPlanScan)
    );
    assert!(
        planned
            .table_references()
            .iter()
            .any(|reference| reference.table_name == "orders"
                && reference.access == TableReferenceAccess::Read
                && reference.source == TableReferenceSource::LogicalPlanScan)
    );
}

#[test]
fn table_reference_extractor_covers_projection_filter_and_typed_subqueries() {
    let extractor = TableReferenceExtractor::new();
    let scalar = TypedExpr::new(
        TypedExprKind::ScalarSubquery(Box::new(scan_plan("invoices"))),
        ResolvedType::Integer,
        Span::empty(),
    );
    let in_subquery = TypedExpr::new(
        TypedExprKind::InSubquery {
            expr: Box::new(column_ref("users", "id")),
            subquery: Box::new(scan_plan("orders")),
            negated: false,
        },
        ResolvedType::Boolean,
        Span::empty(),
    );
    let exists = TypedExpr::new(
        TypedExprKind::Exists {
            subquery: Box::new(scan_plan("shipments")),
            negated: false,
        },
        ResolvedType::Boolean,
        Span::empty(),
    );
    let quantified = TypedExpr::new(
        TypedExprKind::Quantified {
            expr: Box::new(column_ref("users", "id")),
            op: BinaryOp::Lt,
            quantifier: alopex_sql::planner::typed_expr::Quantifier::All,
            subquery: Box::new(scan_plan("payments")),
        },
        ResolvedType::Boolean,
        Span::empty(),
    );
    let predicate = TypedExpr::binary_op(
        in_subquery,
        BinaryOp::And,
        TypedExpr::binary_op(
            exists,
            BinaryOp::And,
            quantified,
            ResolvedType::Boolean,
            Span::empty(),
        ),
        ResolvedType::Boolean,
        Span::empty(),
    );
    let plan = LogicalPlan::Filter {
        input: Box::new(LogicalPlan::scan(
            "users".to_string(),
            Projection::Columns(vec![ProjectedColumn::new(scalar)]),
        )),
        predicate,
    };
    let mut diagnostics = Vec::new();

    let references =
        extractor.extract_from_logical_plan(&plan, TableReferenceAccess::Read, &mut diagnostics);

    assert!(diagnostics.is_empty());
    for table in ["users", "invoices", "orders", "shipments", "payments"] {
        assert!(
            references
                .iter()
                .any(|reference| reference.table_name == table),
            "missing reference for {table}"
        );
    }
    assert!(
        references
            .iter()
            .any(|reference| reference.table_name == "users"
                && reference.source == TableReferenceSource::LogicalPlanScan)
    );
    for table in ["invoices", "orders", "shipments", "payments"] {
        assert!(
            references
                .iter()
                .any(|reference| reference.table_name == table
                    && reference.source == TableReferenceSource::TypedExprSubquery)
        );
    }
}

#[test]
fn table_reference_extractor_covers_join_condition_subquery() {
    let extractor = TableReferenceExtractor::new();
    let plan = LogicalPlan::join(
        scan_plan("users"),
        scan_plan("orders"),
        alopex_sql::planner::JoinType::Inner,
        Some(TypedExpr::new(
            TypedExprKind::Exists {
                subquery: Box::new(scan_plan("audit_log")),
                negated: false,
            },
            ResolvedType::Boolean,
            Span::empty(),
        )),
        None,
    );
    let mut diagnostics = Vec::new();

    let references =
        extractor.extract_from_logical_plan(&plan, TableReferenceAccess::Read, &mut diagnostics);

    assert!(diagnostics.is_empty());
    for table in ["users", "orders", "audit_log"] {
        assert!(
            references
                .iter()
                .any(|reference| reference.table_name == table),
            "missing reference for {table}"
        );
    }
    assert!(
        references
            .iter()
            .any(|reference| reference.table_name == "audit_log"
                && reference.source == TableReferenceSource::TypedExprSubquery)
    );
}

#[test]
fn table_reference_extractor_reports_no_physical_table_reference() {
    let extractor = TableReferenceExtractor::new();
    let mut diagnostics = Vec::new();

    let references = extractor.extract_from_logical_plan(
        &LogicalPlan::scan(LITERAL_TABLE.to_string(), Projection::All(Vec::new())),
        TableReferenceAccess::Read,
        &mut diagnostics,
    );

    assert!(references.is_empty());
    assert_eq!(diagnostics.len(), 1);
    assert_eq!(diagnostics[0].code, "ALOPEX-PLAN-ROUTE-001");
    assert_eq!(diagnostics[0].severity, PlanningDiagnosticSeverity::Info);
}

#[test]
fn table_reference_extractor_reports_unsupported_drop_index_target() {
    let extractor = TableReferenceExtractor::new();
    let mut diagnostics = Vec::new();

    let references = extractor.extract_from_logical_plan(
        &LogicalPlan::drop_index("idx_orders_user_id".to_string(), false),
        TableReferenceAccess::Metadata,
        &mut diagnostics,
    );

    assert!(references.is_empty());
    let drop_index_diagnostic = diagnostics
        .iter()
        .find(|diagnostic| diagnostic.code == "ALOPEX-PLAN-ROUTE-003")
        .expect("DROP INDEX target diagnostic");
    assert_eq!(
        drop_index_diagnostic.severity,
        PlanningDiagnosticSeverity::Warning
    );
    assert!(drop_index_diagnostic.message.contains("idx_orders_user_id"));
}

#[test]
fn planning_boundary_uses_transaction_catalog_view_overlay() {
    let store = std::sync::Arc::new(MemoryKV::new());
    let catalog = PersistentCatalog::load(store).unwrap();
    let mut overlay = CatalogOverlay::new();
    overlay.add_table(
        TableFqn::new("default", "default", "pending_users"),
        table("pending_users").with_table_id(42),
    );
    let view = TxnCatalogView::new(&catalog, &overlay);

    let planned = plan_statement_for_routing(&view, &select_from("pending_users")).unwrap();

    assert_eq!(planned.table_references().len(), 1);
    assert_eq!(planned.table_references()[0].table_name, "pending_users");
    assert_eq!(
        planned.table_references()[0].access,
        TableReferenceAccess::Read
    );
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn plan_sql_for_routing_parses_and_plans_without_execution() {
    let mut catalog = MemoryCatalog::new();
    catalog.create_table(table("users")).unwrap();

    let planned = plan_sql_for_routing(&catalog, "SELECT * FROM users").unwrap();

    assert_eq!(planned.len(), 1);
    assert_eq!(planned[0].table_references()[0].table_name, "users");
}

fn scan_plan(name: &str) -> LogicalPlan {
    LogicalPlan::scan(name.to_string(), Projection::All(vec!["id".to_string()]))
}

fn column_ref(table: &str, column: &str) -> TypedExpr {
    TypedExpr::column_ref(
        table.to_string(),
        column.to_string(),
        0,
        ResolvedType::Integer,
        Span::empty(),
    )
}