sqlmodel 0.2.2

SQL databases in Rust, designed to be intuitive and type-safe
Documentation
#![cfg(feature = "c-sqlite-tests")]

use asupersync::runtime::RuntimeBuilder;
use asupersync::{Cx, Outcome};

use sqlmodel::prelude::*;
use sqlmodel_schema::introspect::{Dialect, Introspector};
use sqlmodel_sqlite::SqliteConnection;

fn unwrap_outcome<T>(outcome: Outcome<T, Error>) -> std::result::Result<T, String> {
    match outcome {
        Outcome::Ok(v) => Ok(v),
        Outcome::Err(e) => Err(format!("unexpected error: {e}")),
        Outcome::Cancelled(r) => Err(format!("cancelled: {r:?}")),
        Outcome::Panicked(p) => Err(format!("panicked: {p:?}")),
    }
}

fn compact_sql_fragment(input: &str) -> String {
    input
        .chars()
        .filter(|c| !c.is_whitespace() && *c != '"' && *c != '`')
        .collect::<String>()
        .to_ascii_lowercase()
}

#[test]
fn sqlite_introspector_extracts_check_constraints_from_live_schema() {
    let rt = RuntimeBuilder::current_thread()
        .build()
        .expect("create asupersync runtime");
    let cx = Cx::for_testing();

    rt.block_on(async {
        let conn = SqliteConnection::open_memory().expect("open sqlite memory db");
        let create_sql = r"
            CREATE TABLE heroes (
                id INTEGER PRIMARY KEY,
                age INTEGER NOT NULL,
                kind TEXT,
                CONSTRAINT age_non_negative CHECK (age >= 0),
                CHECK (age <= 150),
                CHECK (kind IN ('A,B', 'C'))
            )
        ";

        unwrap_outcome(conn.execute(&cx, create_sql, &[]).await).expect("create heroes");

        let introspector = Introspector::new(Dialect::Sqlite);
        let table_info = unwrap_outcome(introspector.table_info(&cx, &conn, "heroes").await)
            .expect("introspect heroes");

        assert_eq!(table_info.comment, None);
        assert_eq!(
            table_info.check_constraints.len(),
            3,
            "unexpected checks: {:?}",
            table_info
                .check_constraints
                .iter()
                .map(|c| (&c.name, &c.expression))
                .collect::<Vec<_>>()
        );

        let named = table_info
            .check_constraints
            .iter()
            .find(|c| c.name.as_deref() == Some("age_non_negative"));
        assert!(
            named.is_some(),
            "missing age_non_negative in {:?}",
            table_info
                .check_constraints
                .iter()
                .map(|c| (&c.name, &c.expression))
                .collect::<Vec<_>>()
        );
        assert_eq!(
            named.expect("named check should exist").expression,
            "age >= 0"
        );

        for check in &table_info.check_constraints {
            let expr = check.expression.trim_start();
            assert!(
                !expr
                    .get(..5)
                    .is_some_and(|prefix| prefix.eq_ignore_ascii_case("CHECK")),
                "expression should be normalized without CHECK prefix: {}",
                check.expression
            );
        }

        let normalized: Vec<String> = table_info
            .check_constraints
            .iter()
            .map(|c| compact_sql_fragment(&c.expression))
            .collect();
        assert!(
            normalized.iter().any(|expr| expr == "age<=150"),
            "missing age<=150 check expression in {normalized:?}"
        );
        assert!(
            normalized.iter().any(|expr| expr == "kindin('a,b','c')"),
            "missing kind IN ('A,B','C') check expression in {normalized:?}"
        );
    });
}

#[test]
fn sqlite_introspector_handles_special_character_table_names() {
    let rt = RuntimeBuilder::current_thread()
        .build()
        .expect("create asupersync runtime");
    let cx = Cx::for_testing();

    rt.block_on(async {
        let conn = SqliteConnection::open_memory().expect("open sqlite memory db");
        let table_name = "heroes table-1";
        let create_sql = r#"
            CREATE TABLE "heroes table-1" (
                id INTEGER PRIMARY KEY,
                age INTEGER NOT NULL,
                CHECK (age >= 0)
            )
        "#;

        unwrap_outcome(conn.execute(&cx, create_sql, &[]).await).expect("create quoted table");

        let introspector = Introspector::new(Dialect::Sqlite);
        let names =
            unwrap_outcome(introspector.table_names(&cx, &conn).await).expect("table names");
        assert!(
            names.iter().any(|name| name == table_name),
            "expected table_names to include quoted-name table, got: {names:?}"
        );

        let table_info = unwrap_outcome(introspector.table_info(&cx, &conn, table_name).await)
            .expect("introspect quoted-name table");
        assert_eq!(table_info.name, table_name);
        assert!(
            table_info.columns.iter().any(|c| c.name == "id"),
            "expected id column in {:?}",
            table_info
                .columns
                .iter()
                .map(|c| &c.name)
                .collect::<Vec<_>>()
        );
        assert!(
            table_info
                .check_constraints
                .iter()
                .any(|c| compact_sql_fragment(&c.expression) == "age>=0"),
            "expected CHECK(age>=0), got {:?}",
            table_info
                .check_constraints
                .iter()
                .map(|c| (&c.name, &c.expression))
                .collect::<Vec<_>>()
        );
    });
}