Skip to content

MONEY columns always read as null #39

Description

@aesslinger

Summary

src/extract.rs's extract_value has no explicit case for Type::MONEY. It falls through to the generic fallback branch (_ => match row.try_get::<_, String>(index) { ... }), but String's FromSql::accepts returns false for Type::MONEY (confirmed directly: <String as FromSql>::accepts(&Type::MONEY) == false), so that fallback always fails and every MONEY column value — null or not — is returned as JSON null.

This is a real, user-visible correctness bug: MONEY is listed as a supported type in the README ("Supported PostgreSQL Data Types" table) and src/handlers/ddl.rs's numeric type group (line 277), but reading a MONEY column back always shows null in the data grid.

Builtin's approach (for reference)

The builtin driver (src-tauri/src/drivers/postgres/extract/advanced_types.rs) defines a dedicated wrapper:

/// Represents the total value in "cents" or the fractional unit of the database's locale.
pub struct Money(i64);

impl<'a> FromSql<'a> for Money {
    fn from_sql(_ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
        Ok(Self(<i64 as FromSql>::from_sql(&Type::INT8, raw)?))
    }

    fn accepts(ty: &Type) -> bool {
        matches!(*ty, Type::MONEY)
    }
}

impl From<Money> for JsonValue {
    fn from(value: Money) -> Self {
        i64_to_json(value.0)  // same JS-safe-integer stringification used for INT8
    }
}

And dispatches it in extract/simple.rs:

Type::MONEY => JsonValue::from(from_sql_or_none::<advanced_types::Money>(ty, buf)),

MONEY is wire-encoded identically to INT8 (8-byte big-endian integer, in the smallest fractional unit / "cents") — the builtin just reinterprets the same bytes i64::from_sql would, then reuses the existing i64_to_json JS-safe-integer stringification (same helper this plugin's extract.rs already has for INT8).

Proposed fix

Add a Money newtype + FromSql impl to src/extract.rs (or a small local module) mirroring the builtin's, and an explicit Type::MONEY arm in extract_value's match, reusing the existing i64_to_json helper already in this file. Add a unit test asserting a MONEY column's wire bytes decode to the same JSON shape INT8 would for the same numeric value (plus one exceeding JS_MAX_SAFE_INTEGER to confirm the stringification kicks in, mirroring the builtin's test_money_above_js_safe_becomes_string).

Scope note: write path

The write path (src/binding.rs) also has no MONEY-specific handling on either side (checked: the builtin's binding.rs doesn't special-case MONEY either — inserting/updating a MONEY column falls to a plain TEXT bind on both drivers). That's parity-consistent, not a gap — no action needed there.

Discovery context

Found while auditing this plugin for other builtin-driver parity gaps following #34/#36/#38.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions