alopex-embedded 0.6.0

Embedded database interface for Alopex DB
use alopex_embedded::{
    ColumnDefinition, CreateCatalogRequest, CreateNamespaceRequest, CreateTableRequest, Database,
    Error, TableType, TxnMode,
};
use alopex_sql::ast::ddl::DataType;
use alopex_sql::ExecutionResult;

fn basic_schema() -> Vec<ColumnDefinition> {
    vec![
        ColumnDefinition::new("id", DataType::Integer).with_nullable(false),
        ColumnDefinition::new("name", DataType::Text),
    ]
}

fn ensure_default_catalog(db: &Database) {
    if db.get_catalog("default").is_err() {
        db.create_catalog(CreateCatalogRequest::new("default"))
            .unwrap();
    }
    if db.get_namespace("default", "default").is_err() {
        db.create_namespace(CreateNamespaceRequest::new("default", "default"))
            .unwrap();
    }
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_hierarchy_create_and_list() {
    let db = Database::new();
    ensure_default_catalog(&db);

    db.create_catalog(CreateCatalogRequest::new("main"))
        .unwrap();
    db.create_namespace(CreateNamespaceRequest::new("main", "analytics"))
        .unwrap();
    db.create_table(
        CreateTableRequest::new("events")
            .with_catalog_name("main")
            .with_namespace_name("analytics")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();

    let catalogs = db.list_catalogs().unwrap();
    assert!(catalogs.iter().any(|catalog| catalog.name == "main"));

    let namespaces = db.list_namespaces("main").unwrap();
    assert!(namespaces
        .iter()
        .any(|namespace| namespace.name == "analytics"));

    let tables = db.list_tables("main", "analytics").unwrap();
    assert_eq!(tables.len(), 1);
    assert_eq!(tables[0].name, "events");

    db.create_table(
        CreateTableRequest::new("users")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();
    db.execute_sql("CREATE INDEX idx_users_id ON users (id);")
        .unwrap();
    let indexes = db.list_indexes_simple("users").unwrap();
    assert!(indexes.iter().any(|index| index.name == "idx_users_id"));
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_force_cascade_delete() {
    let db = Database::new();
    db.create_catalog(CreateCatalogRequest::new("main"))
        .unwrap();
    db.create_namespace(CreateNamespaceRequest::new("main", "analytics"))
        .unwrap();
    db.create_table(
        CreateTableRequest::new("events")
            .with_catalog_name("main")
            .with_namespace_name("analytics")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();

    db.delete_catalog("main", true).unwrap();
    let err = db.get_catalog("main").unwrap_err();
    assert!(matches!(err, Error::CatalogNotFound(ref name) if name == "main"));
    assert!(err.to_string().contains("main"));
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_delete_catalog_with_default_namespace_only() {
    let db = Database::new();
    db.create_catalog(CreateCatalogRequest::new("temp"))
        .unwrap();
    db.create_namespace(CreateNamespaceRequest::new("temp", "default"))
        .unwrap();

    db.delete_catalog("temp", false).unwrap();
    let err = db.get_catalog("temp").unwrap_err();
    assert!(matches!(err, Error::CatalogNotFound(name) if name == "temp"));
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_default_object_protection() {
    let db = Database::new();
    let err = db.delete_catalog("default", false).unwrap_err();
    assert!(matches!(err, Error::CannotDeleteDefault(_)));
    let err = db
        .delete_namespace("default", "default", false)
        .unwrap_err();
    assert!(matches!(err, Error::CannotDeleteDefault(_)));
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_txn_overlay_and_commit() {
    let db = Database::new();
    let mut txn = db.begin(TxnMode::ReadWrite).unwrap();

    txn.create_catalog(CreateCatalogRequest::new("main"))
        .unwrap();
    txn.create_namespace(CreateNamespaceRequest::new("main", "analytics"))
        .unwrap();
    txn.create_table(
        CreateTableRequest::new("events")
            .with_catalog_name("main")
            .with_namespace_name("analytics")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();

    let tables = txn.list_tables("main", "analytics").unwrap();
    assert_eq!(tables.len(), 1);
    assert!(matches!(
        db.get_catalog("main"),
        Err(Error::CatalogNotFound(_))
    ));

    txn.commit().unwrap();

    let tables = db.list_tables("main", "analytics").unwrap();
    assert_eq!(tables.len(), 1);
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_txn_rollback_discards_changes() {
    let db = Database::new();
    {
        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
        txn.create_catalog(CreateCatalogRequest::new("main"))
            .unwrap();
        txn.create_namespace(CreateNamespaceRequest::new("main", "analytics"))
            .unwrap();
        txn.create_table(
            CreateTableRequest::new("events")
                .with_catalog_name("main")
                .with_namespace_name("analytics")
                .with_schema(basic_schema())
                .with_primary_key(vec!["id".to_string()]),
        )
        .unwrap();
        txn.rollback().unwrap();
    }

    assert!(matches!(
        db.get_catalog("main"),
        Err(Error::CatalogNotFound(_))
    ));
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_sql_create_table_defaults_to_default_namespace() {
    let db = Database::new();
    ensure_default_catalog(&db);
    db.execute_sql("CREATE TABLE users (id INTEGER PRIMARY KEY);")
        .unwrap();

    let info = db.get_table_info_simple("users").unwrap();
    assert_eq!(info.catalog_name, "default");
    assert_eq!(info.namespace_name, "default");
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_api_and_sql_share_catalog() {
    let db = Database::new();
    ensure_default_catalog(&db);
    db.create_table(
        CreateTableRequest::new("api_table")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();

    let result = db.execute_sql("SELECT id FROM api_table;").unwrap();
    match result {
        ExecutionResult::Query(result) => assert!(result.rows.is_empty()),
        other => panic!("expected query result, got {other:?}"),
    }
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_sql_table_visible_via_api() {
    let db = Database::new();
    ensure_default_catalog(&db);
    db.execute_sql("CREATE TABLE sql_table (id INTEGER PRIMARY KEY);")
        .unwrap();

    let info = db.get_table_info_simple("sql_table").unwrap();
    assert_eq!(info.name, "sql_table");
    assert_eq!(info.catalog_name, "default");
    assert_eq!(info.namespace_name, "default");
}

#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn catalog_integration_error_scenarios() {
    let db = Database::new();

    let err = db.get_catalog("missing").unwrap_err();
    assert!(matches!(err, Error::CatalogNotFound(ref name) if name == "missing"));
    assert!(err.to_string().contains("missing"));

    db.create_catalog(CreateCatalogRequest::new("main"))
        .unwrap();
    let err = db
        .create_catalog(CreateCatalogRequest::new("main"))
        .unwrap_err();
    assert!(matches!(
        err,
        Error::CatalogAlreadyExists(ref name) if name == "main"
    ));
    assert!(err.to_string().contains("main"));

    let err = db.get_namespace("main", "missing").unwrap_err();
    assert!(matches!(
        err,
        Error::NamespaceNotFound(ref catalog, ref namespace)
            if catalog == "main" && namespace == "missing"
    ));
    assert!(err.to_string().contains("main"));
    assert!(err.to_string().contains("missing"));

    db.create_namespace(CreateNamespaceRequest::new("main", "analytics"))
        .unwrap();
    let err = db
        .create_namespace(CreateNamespaceRequest::new("main", "analytics"))
        .unwrap_err();
    assert!(matches!(
        err,
        Error::NamespaceAlreadyExists(catalog, namespace)
            if catalog == "main" && namespace == "analytics"
    ));

    let err = db
        .get_table_info("main", "analytics", "missing")
        .unwrap_err();
    assert!(matches!(err, Error::TableNotFound(name) if name.contains("missing")));

    db.create_table(
        CreateTableRequest::new("events")
            .with_catalog_name("main")
            .with_namespace_name("analytics")
            .with_schema(basic_schema())
            .with_primary_key(vec!["id".to_string()]),
    )
    .unwrap();
    let err = db
        .create_table(
            CreateTableRequest::new("events")
                .with_catalog_name("main")
                .with_namespace_name("analytics")
                .with_schema(basic_schema())
                .with_primary_key(vec!["id".to_string()]),
        )
        .unwrap_err();
    assert!(matches!(
        err,
        Error::TableAlreadyExists(name) if name == "main.analytics.events"
    ));

    let err = db
        .get_index_info("main", "analytics", "events", "missing_idx")
        .unwrap_err();
    assert!(matches!(err, Error::IndexNotFound(name) if name.contains("missing_idx")));

    let err = db.delete_namespace("main", "analytics", false).unwrap_err();
    assert!(matches!(
        err,
        Error::NamespaceNotEmpty(catalog, namespace)
            if catalog == "main" && namespace == "analytics"
    ));

    let err = db.delete_catalog("main", false).unwrap_err();
    assert!(matches!(err, Error::CatalogNotEmpty(name) if name == "main"));

    ensure_default_catalog(&db);
    let err = db
        .create_table(CreateTableRequest::new("no_schema"))
        .unwrap_err();
    assert!(matches!(err, Error::SchemaRequired));

    let err = db
        .create_table(
            CreateTableRequest::new("external")
                .with_table_type(TableType::External)
                .with_schema(basic_schema()),
        )
        .unwrap_err();
    assert!(matches!(err, Error::StorageRootRequired));

    let err = db
        .create_table(
            CreateTableRequest::new("bad_pk")
                .with_catalog_name("default")
                .with_namespace_name("default")
                .with_schema(basic_schema())
                .with_primary_key(vec!["missing".to_string()]),
        )
        .unwrap_err();
    assert!(matches!(err, Error::Core(_)));
    assert!(err.to_string().contains("missing"));

    let err = db.delete_catalog("default", false).unwrap_err();
    assert!(matches!(err, Error::CannotDeleteDefault(_)));
    let err = db
        .delete_namespace("default", "default", false)
        .unwrap_err();
    assert!(matches!(err, Error::CannotDeleteDefault(_)));

    let err = db
        .execute_sql("INSERT INTO missing_table (id) VALUES (1);")
        .unwrap_err();
    assert!(matches!(err, Error::Sql(_)));
    assert_eq!(err.sql_error_code(), Some("ALOPEX-C001"));

    let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
    let err = ro
        .create_catalog(CreateCatalogRequest::new("readonly"))
        .unwrap_err();
    assert!(matches!(err, Error::TxnReadOnly));
}