google-cloud-spanner-derive 0.2.0

Google Cloud Platform spanner client library macro derive.
Documentation
use google_cloud_spanner::bigdecimal::{BigDecimal, Zero};
use serde::{Deserialize, Serialize};
use serial_test::serial;
use std::str::FromStr;
use time::{Date, OffsetDateTime};

use google_cloud_spanner::client::{Client, ClientConfig, Error};
use google_cloud_spanner::mutation::insert_struct;
use google_cloud_spanner::reader::AsyncIterator;
use google_cloud_spanner::statement::Statement;
use google_cloud_spanner_derive::{Query, Table};

#[derive(Table, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct UserCharacter {
    pub user_id: String,
    pub character_id: i64,
    pub level: i64,
    #[spanner(commitTimestamp)]
    pub updated_at: OffsetDateTime,
}

impl Default for UserCharacter {
    fn default() -> Self {
        Self {
            updated_at: OffsetDateTime::UNIX_EPOCH,
            user_id: Default::default(),
            character_id: Default::default(),
            level: Default::default(),
        }
    }
}

#[derive(Table, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct UserItem {
    pub user_id: String,
    pub item_id: i64,
    pub quantity: i64,
    pub updated_at: OffsetDateTime,
}

impl Default for UserItem {
    fn default() -> Self {
        Self {
            updated_at: OffsetDateTime::UNIX_EPOCH,
            user_id: Default::default(),
            item_id: Default::default(),
            quantity: Default::default(),
        }
    }
}

#[derive(Table, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct User {
    pub user_id: String,
    #[spanner(name = "NotNullINT64")]
    pub not_null_int64: i64,
    #[spanner(name = "NullableINT64")]
    pub nullable_int64: Option<i64>,
    pub not_null_float64: f64,
    pub nullable_float64: Option<f64>,
    pub not_null_bool: bool,
    pub nullable_bool: Option<bool>,
    pub not_null_byte_array: Vec<u8>,
    pub nullable_byte_array: Option<Vec<u8>>,
    pub not_null_numeric: BigDecimal,
    pub nullable_numeric: Option<BigDecimal>,
    pub not_null_timestamp: OffsetDateTime,
    pub nullable_timestamp: Option<OffsetDateTime>,
    pub not_null_date: Date,
    pub nullable_date: Option<Date>,
    pub not_null_array: Vec<i64>,
    pub nullable_array: Option<Vec<i64>>,
    pub nullable_string: Option<String>,
    #[spanner(commitTimestamp)]
    pub updated_at: OffsetDateTime,
}

impl Default for User {
    fn default() -> Self {
        Self {
            not_null_timestamp: OffsetDateTime::UNIX_EPOCH,
            not_null_date: OffsetDateTime::UNIX_EPOCH.date(),
            updated_at: OffsetDateTime::UNIX_EPOCH,
            user_id: Default::default(),
            not_null_int64: Default::default(),
            nullable_int64: Default::default(),
            not_null_float64: Default::default(),
            nullable_float64: Default::default(),
            not_null_bool: Default::default(),
            nullable_bool: Default::default(),
            not_null_byte_array: Default::default(),
            nullable_byte_array: Default::default(),
            not_null_numeric: BigDecimal::zero(),
            nullable_numeric: Default::default(),
            nullable_timestamp: Default::default(),
            nullable_date: Default::default(),
            not_null_array: Default::default(),
            nullable_array: Default::default(),
            nullable_string: Default::default(),
        }
    }
}

#[derive(Query)]
pub struct UserBundle {
    pub user_id: String,
    pub user_characters: Vec<UserCharacter>,
    pub user_items: Vec<UserItem>,
}

#[tokio::test]
#[serial]
async fn test_table_derive() -> Result<(), Error> {
    std::env::set_var("SPANNER_EMULATOR_HOST", "localhost:9010");
    let config = ClientConfig::default();
    let client = Client::new(
        "projects/local-project/instances/test-instance/databases/local-database",
        config,
    )
    .await?;

    let now = OffsetDateTime::now_utc().unix_timestamp();
    let user_id = format!("user{now}");
    let user = User {
        user_id: user_id.clone(),
        not_null_numeric: BigDecimal::from_str("-99999999999999999999999999999.999999999").unwrap(),
        ..Default::default()
    };
    client.apply(vec![insert_struct("User", user)]).await?;

    let mut tx = client.read_only_transaction().await?;
    let mut stmt = Statement::new("SELECT * From User WHERE UserID = @UserID");
    stmt.add_param("UserID", &user_id);
    let mut reader = tx.query(stmt).await?;
    if let Some(row) = reader.next().await? {
        let v: User = row.try_into()?;
        assert_eq!(v.user_id, user_id);
        assert_eq!(&v.not_null_numeric.to_string(), "-99999999999999999999999999999.999999999");
        assert!(v.updated_at.unix_timestamp() >= now);
        let json_string = serde_json::to_string(&v).unwrap();
        let des = serde_json::from_str::<User>(json_string.as_str()).unwrap();
        assert_eq!(des, v);
    } else {
        panic!("no data found");
    }
    Ok(())
}

#[tokio::test]
#[serial]
async fn test_query_derive() -> Result<(), Error> {
    std::env::set_var("SPANNER_EMULATOR_HOST", "localhost:9010");
    let config = ClientConfig::default();
    let client = Client::new(
        "projects/local-project/instances/test-instance/databases/local-database",
        config,
    )
    .await?;

    let now = OffsetDateTime::now_utc().unix_timestamp();
    let user_id = format!("user-q-{now}");
    let user = User {
        user_id: user_id.clone(),
        ..Default::default()
    };
    let user_character = UserCharacter {
        user_id: user_id.clone(),
        ..Default::default()
    };
    let user_item = UserItem {
        user_id: user_id.clone(),
        ..Default::default()
    };
    client
        .apply(vec![
            insert_struct("User", user),
            insert_struct("UserCharacter", user_character),
            insert_struct("UserItem", user_item),
        ])
        .await?;

    let mut tx = client.read_only_transaction().await?;
    let mut stmt = Statement::new(
        "
    SELECT
        UserId,
	    ARRAY(SELECT AS STRUCT * FROM UserCharacter WHERE UserId = @UserId) AS UserCharacters,
	    ARRAY(SELECT AS STRUCT * FROM UserItem WHERE UserId = @UserId) AS UserItems,
    From User
    WHERE UserID = @UserID",
    );
    stmt.add_param("UserID", &user_id);
    let mut reader = tx.query(stmt).await?;
    if let Some(row) = reader.next().await? {
        let v: UserBundle = row.try_into()?;
        assert_eq!(v.user_id, user_id);
        assert_eq!(v.user_characters.len(), 1);
        assert_eq!(v.user_items.len(), 1);
    } else {
        panic!("no data found");
    }
    Ok(())
}