alopex-sql 0.6.0

SQL parser components for the Alopex DB dialect
#![cfg(all(feature = "tokio", not(target_arch = "wasm32")))]

use std::sync::{Arc, RwLock};

use alopex_core::KVStore;
use alopex_core::kv::memory::MemoryKV;
use alopex_core::types::TxnMode;
use alopex_core::vector::hnsw::HnswIndex;
use alopex_sql::ast::ddl::IndexMethod;
use alopex_sql::catalog::{ColumnMetadata, IndexMetadata, PersistentCatalog, TableMetadata};
use alopex_sql::executor::ExecutionResult;
use alopex_sql::planner::typed_expr::{TypedExpr, TypedExprKind};
use alopex_sql::planner::types::ResolvedType;
use alopex_sql::{AsyncSqlTransaction, AsyncTxnBridge, LogicalPlan, Span, TokioAsyncTxnBridge};

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[tokio::test]
async fn async_txn_preserves_hnsw_index_consistency() {
    let store = Arc::new(MemoryKV::new());
    let catalog = Arc::new(RwLock::new(PersistentCatalog::new(store.clone())));

    let bridge = TokioAsyncTxnBridge::new(store.clone(), catalog.clone());
    let mut txn = bridge.begin_write().await.unwrap();

    let table = TableMetadata::new(
        "items",
        vec![
            ColumnMetadata::new("id", ResolvedType::Integer)
                .with_primary_key(true)
                .with_not_null(true),
            ColumnMetadata::new(
                "embedding",
                ResolvedType::Vector {
                    dimension: 2,
                    metric: alopex_sql::ast::ddl::VectorMetric::Cosine,
                },
            )
            .with_not_null(true),
        ],
    )
    .with_primary_key(vec!["id".into()]);

    let create_table = LogicalPlan::CreateTable {
        table,
        if_not_exists: false,
        with_options: vec![],
    };
    let result = txn.execute_plan(create_table).await.unwrap();
    assert!(matches!(result, ExecutionResult::Success));

    let index = IndexMetadata::new(0, "idx_items_embedding", "items", vec!["embedding".into()])
        .with_method(IndexMethod::Hnsw);
    let create_index = LogicalPlan::CreateIndex {
        index,
        if_not_exists: false,
    };
    let result = txn.execute_plan(create_index).await.unwrap();
    assert!(matches!(result, ExecutionResult::Success));
    txn.commit().await.unwrap();

    let mut txn = bridge.begin_write().await.unwrap();
    let insert = LogicalPlan::Insert {
        table: "items".into(),
        columns: vec!["id".into(), "embedding".into()],
        values: vec![vec![
            TypedExpr::literal(
                alopex_sql::ast::expr::Literal::Number("1".into()),
                ResolvedType::Integer,
                Span::default(),
            ),
            TypedExpr {
                kind: TypedExprKind::VectorLiteral(vec![1.0, 0.0]),
                resolved_type: ResolvedType::Vector {
                    dimension: 2,
                    metric: alopex_sql::ast::ddl::VectorMetric::Cosine,
                },
                span: Span::default(),
            },
        ]],
    };
    let result = txn.execute_plan(insert).await.unwrap();
    assert!(matches!(result, ExecutionResult::RowsAffected(1)));
    txn.commit().await.unwrap();

    let mut txn = bridge.begin_write().await.unwrap();
    let insert = LogicalPlan::Insert {
        table: "items".into(),
        columns: vec!["id".into(), "embedding".into()],
        values: vec![vec![
            TypedExpr::literal(
                alopex_sql::ast::expr::Literal::Number("2".into()),
                ResolvedType::Integer,
                Span::default(),
            ),
            TypedExpr {
                kind: TypedExprKind::VectorLiteral(vec![0.0, 1.0]),
                resolved_type: ResolvedType::Vector {
                    dimension: 2,
                    metric: alopex_sql::ast::ddl::VectorMetric::Cosine,
                },
                span: Span::default(),
            },
        ]],
    };
    let result = txn.execute_plan(insert).await.unwrap();
    assert!(matches!(result, ExecutionResult::RowsAffected(1)));
    txn.rollback().await.unwrap();

    let mut read_txn = store.begin(TxnMode::ReadOnly).unwrap();
    let index = HnswIndex::load("idx_items_embedding", &mut read_txn).unwrap();
    let (results, _) = index.search(&[0.0, 1.0], 1, None).unwrap();
    assert_eq!(results.len(), 1);
    let key: [u8; 8] = results[0].key.as_slice().try_into().unwrap();
    assert_eq!(u64::from_be_bytes(key), 1);
}