Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding bigdecimal::BigDecimal as specta compatible type results in string. #25

Closed
marcustut opened this issue Aug 8, 2022 · 4 comments

Comments

@marcustut
Copy link
Contributor

I was using sqlx with MySQL through RSPC. The database is connected successfully, however there are some types that are not currently supported for example chrono::NaiveDate, bigdecimal::BigDecimal. A list of what sqlx maps SQL types into rust types can be found here.

As a workaround, I could get it working by creating another struct and manually mapping the values into the values I need, see following:

// this struct contains the results fetched from db
#[derive(Debug, Serialize)]
struct ExpenseQueryResult {
    id: i32,
    name: String,
    amount: BigDecimal,
    date: chrono::NaiveDate,
    category: String,
    comment: Option<String>,
    tags: Option<String>,
}

// this is an additional struct for mapping unsupported values into supported values
#[derive(Debug)]
struct ListExpensesResponse {
    id: i32,
    name: String,
    amount: f64,
    date: String,
    category: String,
    comment: Option<String>,
    tags: Option<String>,
}

// my router definition
let router = rspc::Router::<Ctx>::new()
    .config(Config::new().export_ts_bindings("../generated/bindings.ts"))
    .query("listExpenses", |ctx: Ctx, _: ()| async move {
        let expenses: Vec<ExpenseQueryResult> = sqlx::query_as!(
            ExpenseQueryResult,
            r#"
select
    e. `id`,
    e. `name`,
    e. `amount`,
    e. `date`,
    ec. `name` as `category`,
    e. `comment`,
    group_concat(t. `name` separator ', ') as `tags`
from
    `expenses` e
    inner join `expenses_categories` ec on e. `expenses_categories_id` = ec. `id`
    left join `tags_expenses` te on e. `id` = te. `expenses_id`
    left join `tags` t on t. `id` = te. `tags_id`
group by
    e. `id`,
    e. `name`,
    e. `amount`,
    e. `date`,
    ec. `name`,
    e. `comment`
order by
    e. `date` desc;
        "#,
        )
        .fetch_all(&ctx.pool)
        .await
        .unwrap();

        selection!(
            expenses
                .into_iter()
                .map(|e| {
                    ListExpensesResponse {
                        id: e.id,
                        name: e.name,
                        amount: e.amount.to_f64().unwrap_or(-1.0),
                        date: e.date.to_string(),
                        category: e.category,
                        comment: e.comment,
                        tags: e.tags,
                    }
                })
                .collect::<Vec<ListExpensesResponse>>(),
            [{ id, name, amount, date, category, comment, tags }]
        )
    })
    .build()
    .arced();

So this code above is working well and fine and produce the following JSON response:

[
	{
		"type": "response",
		"id": null,
		"result": [
			{
				"amount": 118.00,
				"category": "Tithe",
				"comment": "Any comments",
				"date": "2022-08-06",
				"id": 4,
				"name": "August Tithe",
				"tags": "Touch n' Go"
			}
		]
	}
]

However, since I'm learning Rust and wanted to try making chrono::NaiveDate and bigdecimal::BigDecimal works by implementing the rspc::internal::specta::Type trait for it.

The following shows my implementation

#[derive(Debug, Serialize)]
struct SpectaCompatibleNaiveDate(chrono::NaiveDate);

impl From<chrono::NaiveDate> for SpectaCompatibleNaiveDate {
    fn from(date: chrono::NaiveDate) -> Self {
        Self(date)
    }
}

impl rspc::internal::specta::Type for SpectaCompatibleNaiveDate {
    const NAME: &'static str = "NaiveDate";

    fn inline(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::String)
    }

    fn reference(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::String)
    }

    fn definition(_: rspc::internal::specta::DefOpts) -> rspc::internal::specta::DataType {
        panic!()
    }
}

#[derive(Debug, Serialize)]
struct SpectaCompatibleBigDecimal(bigdecimal::BigDecimal);

impl From<bigdecimal::BigDecimal> for SpectaCompatibleBigDecimal {
    fn from(decimal: bigdecimal::BigDecimal) -> Self {
        Self(decimal)
    }
}

impl rspc::internal::specta::Type for SpectaCompatibleBigDecimal {
    const NAME: &'static str = "BigDecimal";

    fn inline(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::f64)
    }

    fn reference(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::f64)
    }

    fn definition(_: rspc::internal::specta::DefOpts) -> rspc::internal::specta::DataType {
        panic!()
    }
}

For SpectaCompatibleNaiveDate, it's working flawlessly. However for SpectaCompatibleBigDecimal, it returned String to the client instead of a number value. Here's the JSON response being returned.

[
	{
		"type": "response",
		"id": null,
		"result": [
			{
				"amount": "118.00",
				"category": "Tithe",
				"comment": "Any comments",
				"date": "2022-08-06",
				"id": 4,
				"name": "August Tithe",
				"tags": "Touch n' Go"
			}
		]
	}
]
@oscartbeaumont
Copy link
Owner

Chrono is officially supported (and used by Spacedrive) with the chrono feature on the crate (do something like rspc = { version = "...", features = ["chrono"] }). It is behind a feature flag so the crate is faster to install and compile for people who don't use that specific features. I am aware the supported features are not documented, it is something I will get to in time. Check out all the other feature flags here.

The issue you're having with the bigdecimal type being a string is coming from the impl serde::Serialize for BigDecimal here. Specta is only responsible for generating Typescript, serde is responsible for the JSON. If I had to guess they are using a string because if they used a bigint it is possible for the number to be truncated, drift or something else to do with floating point numbers. If Specta has an official implementation for bigdecimal I would follow this convention both because it would mean the Typescript matches the JSON and because the crates developers have intentionally done it this way. I would recommend you just convert the string to a number on the frontend or stick with the conversion on the backend for now.

Thanks for sending that sqlx list of supported types, we are missing quite a few of them and I will work on adding them all into Specta. I will put each crate we support as it's own feature flag and then do an sqlx feature flag so you can easily add all of them at once for convenience.

Be aware that the specta::Type trait has no stability guarantee which means we may change how it works between updates. The changes will be documented in the GitHub Release notes so this shouldn't be a major issues but is something to note.

@marcustut
Copy link
Contributor Author

Thanks for the swift response! I've tried the chrono feature flag earlier but it seems like only chrono::DateTime is supported. And thanks for the reference to serde's implementation for BigDecimal, that cleared things up.

@oscartbeaumont
Copy link
Owner

I just added the majority of the sqlx types, exceptions are tracked in #26. The main branch isn't ready for a release yet so I will leave this issue open until these changes are published but feel free to add the following to the bottom of the Cargo.toml to use the bleeding edge version with these fixes:

[patch.crates-io]
rspc = { git = "https://github.com/oscartbeaumont/rspc.git" }
specta = { git = "https://github.com/oscartbeaumont/rspc.git" }

I also changed my mind about an sqlx feature flag as a shortcut for all types used by sqlx. You have to conditionally add all of the features to sqlx so it seems reasonable that a user does the same with rspc. This approach will also save them some compile time which is an added bonus.

@marcustut
Copy link
Contributor Author

I just added the majority of the sqlx types, exceptions are tracked in #26. The main branch isn't ready for a release yet so I will leave this issue open until these changes are published but feel free to add the following to the bottom of the Cargo.toml to use the bleeding edge version with these fixes:

[patch.crates-io]
rspc = { git = "https://github.com/oscartbeaumont/rspc.git" }
specta = { git = "https://github.com/oscartbeaumont/rspc.git" }

I also changed my mind about an sqlx feature flag as a shortcut for all types used by sqlx. You have to conditionally add all of the features to sqlx so it seems reasonable that a user does the same with rspc. This approach will also save them some compile time which is an added bonus.

Thanks for the work! I can verify that these types work now. I think we can close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants