sqlx-turso-driver 0.0.1

An asynchronous SQLx driver for embedded Turso databases
use sqlx::{ConnectOptions, Connection, Executor, Statement};
use sqlx_turso_driver::{Turso, TursoConnectOptions};

// Catches lost writes, the wrong engine, and URL-mangled filesystem paths.
#[tokio::test]
async fn unicode_file_reopens() -> Result<(), Box<dyn std::error::Error>> {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("中文 数据库.db");
    let options = TursoConnectOptions::file(&path)?;
    let mut connection = options.connect().await?;
    connection
        .execute("CREATE TABLE items(value INTEGER NOT NULL)")
        .await?;
    connection
        .execute("INSERT INTO items(value) VALUES (7)")
        .await?;
    connection.close().await?;

    assert!(path.is_file(), "the exact requested path must be used");
    let mut reopened = options.connect().await?;
    let value = sqlx::query_scalar::<Turso, i64>("SELECT value FROM items")
        .fetch_one(&mut reopened)
        .await?;
    assert_eq!(value, 7);
    reopened.close().await?;
    Ok(())
}

// Catches accidental sharing of an in-memory database across connections.
#[tokio::test]
async fn memory_connections_are_independent() -> Result<(), Box<dyn std::error::Error>> {
    let options = TursoConnectOptions::memory();
    let mut first = options.connect().await?;
    let mut second = options.connect().await?;
    first
        .execute("CREATE TABLE items(value INTEGER NOT NULL)")
        .await?;
    first.execute("INSERT INTO items(value) VALUES (7)").await?;
    let value = sqlx::query_scalar::<Turso, i64>("SELECT value FROM items")
        .fetch_one(&mut first)
        .await?;
    assert_eq!(value, 7);
    assert!(
        sqlx::query_scalar::<Turso, i64>("SELECT value FROM items")
            .fetch_one(&mut second)
            .await
            .is_err()
    );
    first.close().await?;
    second.close().await?;
    Ok(())
}

// Catches URL encoding being passed literally to the filesystem or decoded twice.
#[tokio::test]
async fn local_url_roundtrips_exact_file() -> Result<(), Box<dyn std::error::Error>> {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("中文 空格 %25 #?.db");
    let unrelated = directory.path().join("unrelated.txt");
    std::fs::write(&unrelated, b"do not replace")?;
    let options = TursoConnectOptions::file(&path)?;
    let url = options.to_url_lossy();
    let parsed: TursoConnectOptions = url.as_str().parse()?;
    let mut connection = parsed.connect().await?;
    connection
        .execute("CREATE TABLE items(value INTEGER NOT NULL)")
        .await?;
    connection.execute("INSERT INTO items VALUES (7)").await?;
    connection.close().await?;
    assert!(path.is_file());
    assert_eq!(std::fs::read(&unrelated)?, b"do not replace");
    let mut reopened = options.connect().await?;
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT value FROM items")
            .fetch_one(&mut reopened)
            .await?,
        7
    );
    reopened.close().await?;
    Ok(())
}

// Catches treating a documented memory URL as a file or rejecting it wholesale.
#[tokio::test]
async fn memory_url_connects() -> Result<(), Box<dyn std::error::Error>> {
    let options: TursoConnectOptions = "turso-memory:".parse()?;
    let mut connection = options.connect().await?;
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT 7")
            .fetch_one(&mut connection)
            .await?,
        7
    );
    connection.close().await?;
    Ok(())
}

// Catches Turso prepare executing the first SQL statement and silently dropping the rest.
#[tokio::test]
async fn batches_are_rejected_without_partial_execution() -> Result<(), Box<dyn std::error::Error>>
{
    let mut connection = TursoConnectOptions::memory().connect().await?;
    assert!(
        connection
            .execute("CREATE TABLE ignored(value INTEGER); INSERT INTO ignored VALUES (7)")
            .await
            .is_err()
    );
    assert!(
        sqlx::query_scalar::<Turso, i64>("SELECT value FROM ignored")
            .fetch_one(&mut connection)
            .await
            .is_err()
    );
    // A semicolon inside a literal or comment is not a batch.
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT length('a;b'); -- trailing ; comment\n")
            .fetch_one(&mut connection)
            .await?,
        3
    );
    connection.close().await?;
    Ok(())
}

// Catches hiding real database errors, type errors, NULLs or absent rows as fabricated values.
#[tokio::test]
async fn query_failures_are_not_success() -> Result<(), Box<dyn std::error::Error>> {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    let error = sqlx::query_scalar::<Turso, i64>("SELECT value FROM nonexistent")
        .fetch_one(&mut connection)
        .await
        .unwrap_err();
    let sqlx::Error::Database(error) = error else {
        panic!("expected a real database error");
    };
    assert!(std::error::Error::source(&*error).is_some());
    assert_eq!(error.kind(), sqlx::error::ErrorKind::Other);
    assert!(matches!(
        sqlx::query_scalar::<Turso, i64>("SELECT 7 WHERE 0")
            .fetch_one(&mut connection)
            .await,
        Err(sqlx::Error::RowNotFound)
    ));
    assert!(matches!(
        sqlx::query_scalar::<Turso, i64>("SELECT '7'")
            .fetch_one(&mut connection)
            .await,
        Err(sqlx::Error::ColumnDecode { .. })
    ));
    assert!(matches!(
        sqlx::query_scalar::<Turso, i64>("SELECT NULL")
            .fetch_one(&mut connection)
            .await,
        Err(sqlx::Error::ColumnDecode { .. })
    ));
    // D4 enables top-level transactions; retain the surrounding error/NULL
    // and preparation guards while replacing only the old rejecting-shell assertions.
    connection.begin().await?.rollback().await?;
    connection.begin_with("BEGIN").await?.rollback().await?;
    let prepared = connection
        .prepare(sqlx::SqlSafeStr::into_sql_str("SELECT 7"))
        .await?;
    assert_eq!(
        prepared
            .query_scalar::<i64>()
            .fetch_one(&mut connection)
            .await?,
        7
    );
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT 7")
            .fetch_one(&mut connection)
            .await?,
        7
    );
    connection.close().await?;
    Ok(())
}

// Catches delegating remote-host policy to the OS converter, which permits Windows UNC paths.
#[test]
fn file_url_remote_authorities_are_rejected() {
    for value in [
        "file://example.invalid/share/db",
        "file://EXAMPLE.INVALID/share/db",
        "file://localhost.invalid/share/db",
        "file://localhost./share/db",
        "file://127.0.0.1/share/db",
        "file://[::1]/share/db",
    ] {
        let url = url::Url::parse(value).unwrap();
        assert!(
            matches!(
                TursoConnectOptions::from_url(&url),
                Err(sqlx::Error::Configuration(_))
            ),
            "accepted remote authority through from_url: {value}"
        );
        assert!(
            matches!(
                value.parse::<TursoConnectOptions>(),
                Err(sqlx::Error::Configuration(_))
            ),
            "accepted remote authority through FromStr: {value}"
        );
    }
}

// Catches an overbroad authority guard rejecting local URLs or changing their paths.
#[test]
fn file_url_local_authorities_are_accepted() -> Result<(), Box<dyn std::error::Error>> {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("local.db");
    let expected = url::Url::from_file_path(&path).unwrap();
    for authority in ["", "localhost", "LOCALHOST"] {
        let value = expected
            .as_str()
            .replacen("file://", &format!("file://{authority}"), 1);
        let url = url::Url::parse(&value)?;
        assert_eq!(
            TursoConnectOptions::from_url(&url)?.to_url_lossy(),
            expected
        );
        assert_eq!(
            value.parse::<TursoConnectOptions>()?.to_url_lossy(),
            expected
        );
    }
    Ok(())
}

// Catches silently accepting unsupported flags or remote authorities on local URLs.
#[test]
fn local_url_flags_are_rejected() {
    for url in [
        "file://example.invalid/db",
        "file:///tmp/db?mode=ro",
        "file:///tmp/db#fragment",
        "turso-memory:shared",
        "turso-memory:?cache=shared",
    ] {
        assert!(
            url.parse::<TursoConnectOptions>().is_err(),
            "accepted {url}"
        );
    }
}

// Constructor characterization only; no filesystem open or fake RED required.
#[test]
fn file_constructor_rejects_empty_path() {
    assert!(matches!(
        TursoConnectOptions::file(""),
        Err(sqlx::Error::Configuration(_))
    ));
}

#[cfg(unix)]
#[test]
fn file_constructor_rejects_non_utf8_path() {
    use std::os::unix::ffi::OsStringExt;
    let path = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![0xff]));
    assert!(matches!(
        TursoConnectOptions::file(path),
        Err(sqlx::Error::Configuration(_))
    ));
}

// Literal :memory: is made absolute, NOT rejected. Never open the cwd path or
// mutate global cwd; actual persistence uses an owned directory's reserved name.
#[tokio::test]
async fn reserved_memory_file_name_is_absolute_and_persistent()
-> Result<(), Box<dyn std::error::Error>> {
    let literal = TursoConnectOptions::file(":memory:")?;
    assert_eq!(
        literal.to_url_lossy().to_file_path().unwrap(),
        std::env::current_dir()?.join(":memory:")
    );
    let directory = tempfile::tempdir()?;
    let path = directory.path().join(":memory:");
    let options = TursoConnectOptions::file(&path)?;
    let mut connection = options.connect().await?;
    connection
        .execute("CREATE TABLE items(value INTEGER)")
        .await?;
    connection.execute("INSERT INTO items VALUES (7)").await?;
    connection.close().await?;
    assert!(path.is_file());
    let mut reopened = options.connect().await?;
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT value FROM items")
            .fetch_one(&mut reopened)
            .await?,
        7
    );
    reopened.close().await?;
    Ok(())
}

// Catches accepting remote URLs and silently treating them as local files.
#[test]
fn remote_and_unknown_urls_are_rejected() {
    for url in [
        "https://example.invalid/db",
        "libsql://example.invalid/db",
        "turso://example.invalid/db",
        "unknown://db",
    ] {
        assert!(
            url.parse::<TursoConnectOptions>().is_err(),
            "accepted {url}"
        );
    }
}