drizzle 0.1.15

A type-safe SQL query builder for Rust
Documentation
#![cfg(any(feature = "rusqlite", feature = "turso", feature = "libsql"))]

use drizzle::core::expr::*;
use drizzle::error::DrizzleError;
use drizzle::sqlite::prelude::*;
use drizzle::sqlite::traits::DrizzleSQLiteColumn;
use std::borrow::Cow;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct U32Be(u32);

impl DrizzleSQLiteColumn for U32Be {
    type SQLType = drizzle::sqlite::types::Blob;

    const SQL_TYPE: &'static str = "BLOB";

    fn decode(value: SQLiteValueRef<'_>) -> Result<Self, DrizzleError> {
        let SQLiteValueRef::Blob(value) = value else {
            return Err(DrizzleError::ConversionError(
                "U32Be must be stored as BLOB".into(),
            ));
        };

        let bytes: [u8; 4] = value.try_into().map_err(|_| {
            DrizzleError::ConversionError(
                format!("expected 4 bytes for U32Be, got {}", value.len()).into(),
            )
        })?;
        Ok(Self(u32::from_be_bytes(bytes)))
    }

    fn encode(&self) -> SQLiteValue<'_> {
        SQLiteValue::Blob(Cow::Owned(self.0.to_be_bytes().to_vec()))
    }

    fn encode_owned(self) -> OwnedSQLiteValue {
        OwnedSQLiteValue::Blob(Box::new(self.0.to_be_bytes()))
    }
}

#[test]
fn custom_sqlite_column_decode_rejects_invalid_storage() {
    assert!(U32Be::decode(SQLiteValueRef::Text("1")).is_err());
    assert!(U32Be::decode(SQLiteValueRef::Blob(&[1, 2, 3])).is_err());
}

#[test]
fn custom_sqlite_column_converts_from_borrowed_value_ref() {
    let bytes = [0x01, 0x02, 0x03, 0x04];
    let value = SQLiteValue::Blob(Cow::Borrowed(&bytes));

    assert_eq!(value.convert::<U32Be>().unwrap(), U32Be(0x0102_0304));
}

#[SQLiteTable(NAME = "custom_u32_be_test")]
struct CustomU32BeTest {
    #[column(PRIMARY)]
    id: i32,
    payload: U32Be,
    optional_payload: Option<U32Be>,
}

#[derive(SQLiteSchema)]
struct CustomU32BeSchema {
    custom_u32_be_test: CustomU32BeTest,
}

struct CustomU32BeAlias;

impl drizzle::core::Tag for CustomU32BeAlias {
    const NAME: &'static str = "custom_u32_alias";
}

#[drizzle::test]
fn custom_sqlite_column_roundtrip_and_metadata(db: &mut TestDb<CustomU32BeSchema>) {
    let table = schema.custom_u32_be_test;

    assert!(CustomU32BeTest::ddl_sql().contains("`payload` BLOB NOT NULL"));
    assert_eq!(
        <CustomU32BeTest as drizzle::core::DrizzleTable>::TABLE_REF.columns[1].sql_type,
        "BLOB"
    );

    let value = U32Be(0x0102_0304);
    let row = InsertCustomU32BeTest::new(value).with_optional_payload(SQLiteInsertValue::Null);
    db.insert(table).values([row]).execute();

    let out: Vec<SelectCustomU32BeTest> = db
        .select((table.id, table.payload, table.optional_payload))
        .from(table)
        .r#where(and(
            and(eq(table.payload, value), eq(table.payload, &value)),
            is_null(table.optional_payload),
        ))
        .all();

    assert_eq!(out.len(), 1);
    assert_eq!(out[0].payload, value);
    assert_eq!(out[0].optional_payload, None);

    let optional_matches: Vec<SelectCustomU32BeTest> = db
        .select((table.id, table.payload, table.optional_payload))
        .from(table)
        .r#where(eq(table.optional_payload, value))
        .all();

    assert!(optional_matches.is_empty());

    let alias = CustomU32BeTest::alias::<CustomU32BeAlias>();
    let aliased: Vec<SelectCustomU32BeTest> = db
        .select((alias.id, alias.payload, alias.optional_payload))
        .from(alias)
        .r#where(eq(alias.payload, value))
        .all();

    assert_eq!(aliased.len(), 1);
    assert_eq!(aliased[0].payload, value);

    #[derive(SQLiteFromRow, Debug)]
    struct Ty(String);
    let ty: Ty = db
        .select(r#typeof(table.payload).alias("payload_type"))
        .from(table)
        .r#where(eq(table.id, 1))
        .get();

    assert_eq!(ty.0, "blob");
}