sqlx-turso-driver 0.0.1

An asynchronous SQLx driver for embedded Turso databases
use sqlx::{
    Arguments, ConnectOptions, Decode, Encode, Executor, Row, Statement, Type, Value, ValueRef,
};
use sqlx_core::{encode::IsNull, error::BoxDynError};
use sqlx_turso_driver::{Turso, TursoArguments, TursoConnectOptions, TursoTypeInfo, TursoValueRef};

type TestResult = Result<(), BoxDynError>;

// Catches rejected/discarded binds and mishandling repeated parameter slots.
#[tokio::test]
async fn positional_binding_reaches_engine() -> TestResult {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT ?1 + ?1")
            .bind(7_i64)
            .fetch_one(&mut connection)
            .await?,
        14
    );
    assert_eq!(
        sqlx::query_scalar::<Turso, i64>("SELECT ? + ?")
            .bind(7_i32)
            .bind(8_u8)
            .fetch_one(&mut connection)
            .await?,
        15
    );
    Ok(())
}

// Catches conflating NULL with empty buffers, lossy UTF-8/blob copies and SQL substitution.
#[tokio::test]
async fn null_and_empty_are_distinct() -> TestResult {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    connection
        .execute("CREATE TABLE items(t TEXT, b BLOB, n INTEGER)")
        .await?;
    for (text, blob, number) in [
        (None, None, None),
        (Some(""), Some(&b""[..]), Some(i64::MIN)),
        (
            Some("中文 '); DROP TABLE items; --"),
            Some(&b"\0\xff\0abc"[..]),
            Some(i64::MAX),
        ),
    ] {
        sqlx::query::<Turso>("INSERT INTO items VALUES (?, ?, ?)")
            .bind(text)
            .bind(blob)
            .bind(number)
            .execute(&mut connection)
            .await?;
    }
    let rows = sqlx::query::<Turso>("SELECT t, b, n FROM items ORDER BY rowid")
        .fetch_all(&mut connection)
        .await?;
    assert_eq!(rows.len(), 3);
    assert_eq!(rows[0].try_get::<Option<String>, _>(0)?, None);
    assert_eq!(rows[0].try_get::<Option<Vec<u8>>, _>(1)?, None);
    assert_eq!(rows[0].try_get::<Option<i64>, _>(2)?, None);
    assert!(rows[0].try_get_raw(0)?.is_null());
    assert!(rows[0].try_get::<String, _>(0).is_err());
    assert_eq!(
        rows[1].try_get::<Option<String>, _>(0)?,
        Some(String::new())
    );
    assert_eq!(rows[1].try_get::<Option<Vec<u8>>, _>(1)?, Some(vec![]));
    assert_eq!(rows[1].try_get::<i64, _>(2)?, i64::MIN);
    assert!(!rows[1].try_get_raw(0)?.is_null());
    assert!(!rows[1].try_get_raw(1)?.is_null());
    assert_eq!(
        rows[2].try_get::<&str, _>(0)?,
        "中文 '); DROP TABLE items; --"
    );
    assert_eq!(rows[2].try_get::<&[u8], _>(1)?, b"\0\xff\0abc");
    assert_eq!(rows[2].try_get::<i64, _>(2)?, i64::MAX);
    let owned = ValueRef::to_owned(&rows[2].try_get_raw(0)?);
    drop(rows);
    assert_eq!(
        <&str as Decode<Turso>>::decode(owned.as_ref())?,
        "中文 '); DROP TABLE items; --"
    );
    Ok(())
}

// Catches wrapping narrow integers, unsigned negatives and out-of-range unsigned encoding.
#[tokio::test]
async fn narrow_integer_overflow_errors() -> TestResult {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    for value in [256_i64, -1] {
        let row = sqlx::query::<Turso>("SELECT ?")
            .bind(value)
            .fetch_one(&mut connection)
            .await?;
        assert!(matches!(
            row.try_get::<u8, _>(0),
            Err(sqlx::Error::ColumnDecode { .. })
        ));
    }
    let row = sqlx::query::<Turso>("SELECT 128, 32768, 2147483648, -1")
        .fetch_one(&mut connection)
        .await?;
    assert!(row.try_get::<i8, _>(0).is_err());
    assert!(row.try_get::<i16, _>(1).is_err());
    assert!(row.try_get::<i32, _>(2).is_err());
    assert!(row.try_get::<u64, _>(3).is_err());
    assert!(matches!(
        sqlx::query::<Turso>("SELECT ?")
            .bind(u64::MAX)
            .fetch_one(&mut connection)
            .await,
        Err(sqlx::Error::Encode(_))
    ));
    let row = sqlx::query::<Turso>("SELECT ?, ?, ?, ?, ?, ?, ?, ?")
        .bind(i8::MIN)
        .bind(i16::MIN)
        .bind(i32::MIN)
        .bind(i64::MIN)
        .bind(u8::MAX)
        .bind(u16::MAX)
        .bind(u32::MAX)
        .bind(i64::MAX as u64)
        .fetch_one(&mut connection)
        .await?;
    assert_eq!(row.try_get::<i8, _>(0)?, -128);
    assert_eq!(row.try_get::<i16, _>(1)?, -32768);
    assert_eq!(row.try_get::<i32, _>(2)?, -2147483648);
    assert_eq!(row.try_get::<i64, _>(3)?, i64::MIN);
    assert_eq!(row.try_get::<u8, _>(4)?, 255);
    assert_eq!(row.try_get::<u16, _>(5)?, 65535);
    assert_eq!(row.try_get::<u32, _>(6)?, 4294967295);
    assert_eq!(row.try_get::<u64, _>(7)?, 9223372036854775807);
    Ok(())
}

// Catches wrong storage classes, dropped owned values and numeric/string coercion.
#[tokio::test]
async fn scalar_codecs_are_strict() -> TestResult {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    let row = sqlx::query::<Turso>("SELECT ?, ?, ?, ?, ?, ?, ?, ?")
        .bind(1.25_f64)
        .bind(-2.5_f32)
        .bind(true)
        .bind(false)
        .bind(String::from("中文"))
        .bind(vec![0_u8, 255, 0])
        .bind(Some(4.5_f64))
        .bind(None::<bool>)
        .fetch_one(&mut connection)
        .await?;
    assert_eq!(row.try_get::<f64, _>(0)?, 1.25);
    assert_eq!(row.try_get::<f32, _>(1)?, -2.5);
    assert!(row.try_get::<bool, _>(2)?);
    assert!(!row.try_get::<bool, _>(3)?);
    assert_eq!(row.try_get::<String, _>(4)?, "中文");
    assert_eq!(row.try_get::<Vec<u8>, _>(5)?, [0, 255, 0]);
    assert_eq!(row.try_get::<Option<f64>, _>(6)?, Some(4.5));
    assert_eq!(row.try_get::<Option<bool>, _>(7)?, None);
    assert!(row.try_get::<i64, _>(0).is_err());
    assert!(row.try_get::<f64, _>(4).is_err());
    assert!(row.try_get::<Vec<u8>, _>(4).is_err());
    assert!(row.try_get::<String, _>(5).is_err());
    assert_eq!(
        row.try_get_raw(0)?.type_info().as_ref(),
        &TursoTypeInfo::Real
    );
    Ok(())
}

// Catches stale bindings across executions of one SQLx Statement.
#[tokio::test]
async fn prepared_statement_never_reuses_old_bindings() -> TestResult {
    use sqlx::SqlSafeStr;
    let mut connection = TursoConnectOptions::memory().connect().await?;
    let statement = connection
        .prepare("SELECT ?1, ?2, ?1".into_sql_str())
        .await?;
    for (first, second) in [(7_i64, Some(8_i64)), (9, None), (10, Some(11))] {
        let row = statement
            .query()
            .bind(first)
            .bind(second)
            .fetch_one(&mut connection)
            .await?;
        assert_eq!(row.try_get::<i64, _>(0)?, first);
        assert_eq!(row.try_get::<Option<i64>, _>(1)?, second);
        assert_eq!(row.try_get::<i64, _>(2)?, first);
    }
    Ok(())
}

// Turso leaves unbound slots NULL; the NOT NULL failure must remain an actual engine error.
#[tokio::test]
async fn insufficient_and_extra_bindings_preserve_engine_errors() -> TestResult {
    let mut connection = TursoConnectOptions::memory().connect().await?;
    connection
        .execute("CREATE TABLE items(a INTEGER, b INTEGER NOT NULL)")
        .await?;
    let error = sqlx::query::<Turso>("INSERT INTO items VALUES (?1, ?2)")
        .bind(7_i64)
        .execute(&mut connection)
        .await
        .unwrap_err();
    let sqlx::Error::Database(error) = error else {
        panic!("expected engine error")
    };
    assert!(matches!(
        std::error::Error::source(&*error)
            .unwrap()
            .downcast_ref::<turso::Error>(),
        Some(turso::Error::Constraint(_))
    ));
    let missing = sqlx::query::<Turso>("SELECT ?1, ?2")
        .bind(7_i64)
        .fetch_one(&mut connection)
        .await?;
    assert_eq!(missing.try_get::<Option<i64>, _>(1)?, None);
    let error = sqlx::query::<Turso>("SELECT ?1")
        .bind(7_i64)
        .bind(8_i64)
        .fetch_one(&mut connection)
        .await
        .unwrap_err();
    let sqlx::Error::Database(error) = error else {
        panic!("expected engine error")
    };
    assert!(matches!(
        std::error::Error::source(&*error)
            .unwrap()
            .downcast_ref::<turso::Error>(),
        Some(turso::Error::Misuse(_))
    ));
    Ok(())
}

struct Failing;
impl Type<Turso> for Failing {
    fn type_info() -> TursoTypeInfo {
        TursoTypeInfo::Integer
    }
}
impl Encode<'_, Turso> for Failing {
    fn encode_by_ref(&self, buffer: &mut Vec<turso::Value>) -> Result<IsNull, BoxDynError> {
        buffer.push(turso::Value::Integer(999));
        Err("deliberate encoder failure".into())
    }
}

// Catches partial encoder writes corrupting reusable arguments.
#[tokio::test]
async fn arguments_roll_back_failed_encoding() -> TestResult {
    let mut arguments = TursoArguments::default();
    arguments.reserve(3, 0);
    arguments.add(7_i64)?;
    assert!(arguments.add(Failing).is_err());
    assert_eq!(arguments.len(), 1);
    arguments.add(None::<i64>)?;
    let mut connection = TursoConnectOptions::memory().connect().await?;
    let row = sqlx::query_with::<Turso, _>("SELECT ?1, ?2", arguments)
        .fetch_one(&mut connection)
        .await?;
    assert_eq!(row.try_get::<i64, _>(0)?, 7);
    assert_eq!(row.try_get::<Option<i64>, _>(1)?, None);
    Ok(())
}

// Ensure public associated value-ref types can be used by application codecs.
fn _public_value_ref(value: TursoValueRef<'_>) -> Result<i64, BoxDynError> {
    <i64 as Decode<Turso>>::decode(value)
}