Skip to content

Commit 3f29865

Browse files
authored
fix: cast money columns to numeric (#5705)
[TML-1657](https://linear.app/prisma-company/issue/TML-1657/fix-dbmoney-parsing-issues) Parsing money values returned from Postgres is incredibly complicated due to it being dependent on locale and accepting a huge number of formats. There's an easy trick to avoid all of that: do what we do for enums and cast all money columns to `numeric`. This also allows us to implement it once for both relationJoins and non-relationJoins code and it conforms to our old behavior of always just returning the plain number, while keeping the implementation simple. Fixes prisma/prisma#27570
1 parent ab635e6 commit 3f29865

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

  • quaint/src/visitor
  • query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native

quaint/src/visitor/postgres.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ impl<'a> Visitor<'a> for Postgres<'a> {
151151

152152
/// A database column identifier
153153
fn visit_column(&mut self, column: Column<'a>) -> visitor::Result {
154+
let cast_target = get_column_cast_target(&column);
155+
154156
match column.table {
155157
Some(table) => {
156158
self.visit_table(table, false)?;
@@ -160,11 +162,11 @@ impl<'a> Visitor<'a> for Postgres<'a> {
160162
_ => self.delimited_identifiers(&[&*column.name])?,
161163
};
162164

163-
if column.is_enum && column.is_selected {
165+
if let Some(cast) = cast_target {
166+
self.write("::")?;
167+
self.write(cast)?;
164168
if column.is_list {
165-
self.write("::text[]")?;
166-
} else {
167-
self.write("::text")?;
169+
self.write("[]")?;
168170
}
169171
}
170172

@@ -739,27 +741,29 @@ impl<'a> Visitor<'a> for Postgres<'a> {
739741

740742
fn visit_min(&mut self, min: Minimum<'a>) -> visitor::Result {
741743
// If the inner column is a selected enum, then we cast the result of MIN(enum)::text instead of casting the inner enum column, which changes the behavior of MIN.
742-
let should_cast = min.column.is_enum && min.column.is_selected;
744+
let cast_target = get_column_cast_target(&min.column);
743745

744746
self.write("MIN")?;
745747
self.surround_with("(", ")", |ref mut s| s.visit_column(min.column.set_is_selected(false)))?;
746748

747-
if should_cast {
748-
self.write("::text")?;
749+
if let Some(cast_target) = cast_target {
750+
self.write("::")?;
751+
self.write(cast_target)?;
749752
}
750753

751754
Ok(())
752755
}
753756

754757
fn visit_max(&mut self, max: Maximum<'a>) -> visitor::Result {
755758
// If the inner column is a selected enum, then we cast the result of MAX(enum)::text instead of casting the inner enum column, which changes the behavior of MAX.
756-
let should_cast = max.column.is_enum && max.column.is_selected;
759+
let cast_target = get_column_cast_target(&max.column);
757760

758761
self.write("MAX")?;
759762
self.surround_with("(", ")", |ref mut s| s.visit_column(max.column.set_is_selected(false)))?;
760763

761-
if should_cast {
762-
self.write("::text")?;
764+
if let Some(cast_target) = cast_target {
765+
self.write("::")?;
766+
self.write(cast_target)?;
763767
}
764768

765769
Ok(())
@@ -785,6 +789,20 @@ impl<'a> Visitor<'a> for Postgres<'a> {
785789
}
786790
}
787791

792+
fn get_column_cast_target(column: &Column<'_>) -> Option<&'static str> {
793+
if !column.is_selected {
794+
return None;
795+
}
796+
797+
if column.is_enum {
798+
Some("text")
799+
} else if column.native_type.as_deref() == Some("MONEY") || column.native_type.as_deref() == Some("MONEY[]") {
800+
Some("numeric")
801+
} else {
802+
None
803+
}
804+
}
805+
788806
#[cfg(test)]
789807
mod tests {
790808
use crate::visitor::*;

query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/data_types/native/postgres.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,37 @@ mod postgres_decimal {
134134
}
135135
}
136136

137+
#[test_suite(only(Postgres))]
138+
mod postgres_money {
139+
fn schema_decimal() -> String {
140+
let schema = indoc! {
141+
r#"
142+
model Table {
143+
#id(id, Int, @id)
144+
145+
money Decimal @test.Money
146+
moneyList Decimal[] @test.Money
147+
}"#
148+
};
149+
150+
schema.to_owned()
151+
}
152+
153+
#[connector_test(schema(schema_decimal))]
154+
async fn native_money_type(runner: Runner) -> TestResult<()> {
155+
runner.raw_execute(
156+
r#"INSERT INTO "Table" ("id", "money", "moneyList") VALUES (1, '$300,000.52', array['$100,000.00', '$200.25']::money[]);"#,
157+
).await?;
158+
159+
insta::assert_snapshot!(
160+
run_query!(&runner, r#"{ findManyTable { id money moneyList } }"#),
161+
@r###"{"data":{"findManyTable":[{"id":1,"money":"300000.52","moneyList":["100000","200.25"]}]}}"###
162+
);
163+
164+
Ok(())
165+
}
166+
}
167+
137168
#[test_suite(only(Postgres))]
138169
mod postgres_string {
139170
fn schema_string() -> String {

0 commit comments

Comments
 (0)