Skip to content

Commit b2f3b82

Browse files
committed
fix: correct PostgreSQL casts and partial index arity
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 059a7b2 commit b2f3b82

7 files changed

Lines changed: 102 additions & 13 deletions

File tree

psl/parser-database/src/attributes.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -640,16 +640,16 @@ fn parse_where_clause(model_id: crate::ModelId, ctx: &mut Context<'_>) -> Option
640640

641641
/// Parse raw("...") where clause.
642642
fn parse_raw_where_clause(args: &[ast::Argument], ctx: &mut Context<'_>) -> Option<WhereClause> {
643-
let Some(first_arg) = args.first() else {
643+
let [arg] = args else {
644644
ctx.push_attribute_validation_error(
645-
"The `where` argument must be a raw() function with a string argument, e.g. `where: raw(\"status = 'active'\")`.",
645+
"The `where` argument must be a raw() function with exactly one string argument, e.g. `where: raw(\"status = 'active'\")`.",
646646
);
647647
return None;
648648
};
649649

650-
let Some(predicate) = coerce::string(&first_arg.value, ctx.diagnostics) else {
650+
let Some(predicate) = coerce::string(&arg.value, ctx.diagnostics) else {
651651
ctx.push_attribute_validation_error(
652-
"The `where` argument must be a raw() function with a string argument, e.g. `where: raw(\"status = 'active'\")`.",
652+
"The `where` argument must be a raw() function with exactly one string argument, e.g. `where: raw(\"status = 'active'\")`.",
653653
);
654654
return None;
655655
};

psl/psl/tests/attributes/partial_index.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ fn partial_index_raw_requires_string_argument() {
166166

167167
let err = parse_unwrap_err(&with_header(dml, Provider::Postgres, &["partialIndexes"]));
168168
let expected = expect![[r#"
169-
[1;91merror[0m: [1mError parsing attribute "@unique": The `where` argument must be a raw() function with a string argument, e.g. `where: raw("status = 'active'")`.[0m
169+
[1;91merror[0m: [1mError parsing attribute "@unique": The `where` argument must be a raw() function with exactly one string argument, e.g. `where: raw("status = 'active'")`.[0m
170170
--> schema.prisma:15
171171
 | 
172172
14 | 
@@ -176,6 +176,30 @@ fn partial_index_raw_requires_string_argument() {
176176
expected.assert_eq(&err);
177177
}
178178

179+
#[test]
180+
fn partial_index_raw_rejects_multiple_arguments() {
181+
let dml = indoc! {r#"
182+
model User {
183+
id Int @id
184+
email String
185+
status String
186+
187+
@@unique([email], where: raw("status = 'active'", "email IS NOT NULL"))
188+
}
189+
"#};
190+
191+
let err = parse_unwrap_err(&with_header(dml, Provider::Postgres, &["partialIndexes"]));
192+
let expected = expect![[r#"
193+
error: Error parsing attribute "@unique": The `where` argument must be a raw() function with exactly one string argument, e.g. `where: raw("status = 'active'")`.
194+
--> schema.prisma:15
195+
 | 
196+
14 | 
197+
15 |  @@unique([email], where: raw("status = 'active'", "email IS NOT NULL"))
198+
 | 
199+
"#]];
200+
expected.assert_eq(&err);
201+
}
202+
179203
#[test]
180204
fn partial_index_cannot_have_empty_predicate() {
181205
let dml = indoc! {r#"

quaint/src/visitor/postgres.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,24 @@ impl<'a> Postgres<'a> {
2727
match expr.kind() {
2828
ExpressionKind::Column(col) => match (col.type_family.as_ref(), col.native_type.as_deref()) {
2929
(Some(TypeFamily::Decimal(_)), Some("MONEY")) => {
30+
let is_list = col.is_list;
3031
self.visit_expression(expr)?;
3132
self.write("::numeric")?;
33+
if is_list {
34+
self.write("[]")?;
35+
}
3236

3337
Ok(())
3438
}
3539
// Cast BigInt to text to preserve precision when parsed by JavaScript.
3640
// JavaScript's JSON.parse loses precision for integers > 2^53-1.
3741
(Some(TypeFamily::Int), Some("BIGINT" | "INT8")) => {
42+
let is_list = col.is_list;
3843
self.visit_expression(expr)?;
3944
self.write("::text")?;
45+
if is_list {
46+
self.write("[]")?;
47+
}
4048

4149
Ok(())
4250
}
@@ -1442,6 +1450,22 @@ mod tests {
14421450
assert_eq!(sql, "SELECT JSONB_BUILD_OBJECT('money', \"money\"::numeric)");
14431451
}
14441452

1453+
#[test]
1454+
fn money_array() {
1455+
let build_json = json_build_object(vec![(
1456+
"money".into(),
1457+
Column::from("money")
1458+
.native_column_type(Some("money"))
1459+
.type_family(TypeFamily::Decimal(None))
1460+
.set_is_list(true)
1461+
.into(),
1462+
)]);
1463+
let query = Select::default().value(build_json);
1464+
let (sql, _) = Postgres::build(query).unwrap();
1465+
1466+
assert_eq!(sql, "SELECT JSONB_BUILD_OBJECT('money', \"money\"::numeric[])");
1467+
}
1468+
14451469
#[test]
14461470
fn bigint() {
14471471
let build_json = json_build_object(vec![(
@@ -1457,6 +1481,22 @@ mod tests {
14571481
assert_eq!(sql, "SELECT JSONB_BUILD_OBJECT('id', \"id\"::text)");
14581482
}
14591483

1484+
#[test]
1485+
fn bigint_array() {
1486+
let build_json = json_build_object(vec![(
1487+
"id".into(),
1488+
Column::from("id")
1489+
.native_column_type(Some("BigInt"))
1490+
.type_family(TypeFamily::Int)
1491+
.set_is_list(true)
1492+
.into(),
1493+
)]);
1494+
let query = Select::default().value(build_json);
1495+
let (sql, _) = Postgres::build(query).unwrap();
1496+
1497+
assert_eq!(sql, "SELECT JSONB_BUILD_OBJECT('id', \"id\"::text[])");
1498+
}
1499+
14601500
#[test]
14611501
fn int8() {
14621502
let build_json = json_build_object(vec![(

query-compiler/query-builders/sql-query-builder/src/read.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ fn apply_aggregate_selections(
242242
.field
243243
.as_scalar()
244244
.unwrap()
245-
.as_column_with_style(ctx, col_style))
245+
.as_column_with_style(ctx, col_style)
246+
.set_is_selected(true))
246247
.alias(next_field.db_alias().into_owned()),
247248
)
248249
}),
@@ -253,7 +254,8 @@ fn apply_aggregate_selections(
253254
.field
254255
.as_scalar()
255256
.unwrap()
256-
.as_column_with_style(ctx, col_style))
257+
.as_column_with_style(ctx, col_style)
258+
.set_is_selected(true))
257259
.alias(next_field.db_alias().into_owned()),
258260
)
259261
}),

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ mod postgres_money {
163163

164164
Ok(())
165165
}
166+
167+
#[connector_test(schema(schema_decimal))]
168+
async fn native_money_aggregates(runner: Runner) -> TestResult<()> {
169+
runner
170+
.raw_execute(
171+
r#"INSERT INTO "Table" ("id", "money", "moneyList") VALUES
172+
(1, '$10.25', ARRAY[]::money[]),
173+
(2, '$20.75', ARRAY[]::money[]);"#,
174+
)
175+
.await?;
176+
177+
insta::assert_snapshot!(
178+
run_query!(&runner, r#"{ aggregateTable { _sum { money } _avg { money } } }"#),
179+
@r###"{"data":{"aggregateTable":{"_sum":{"money":"31"},"_avg":{"money":"15.5"}}}}"###
180+
);
181+
182+
Ok(())
183+
}
166184
}
167185

168186
#[test_suite(only(Postgres))]

schema-engine/connectors/sql-schema-connector/src/flavour/postgres/renderer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ fn render_postgres_alter_enum(
974974
let sql = format!(
975975
"ALTER TABLE {table_name} \
976976
ALTER COLUMN {column_name} TYPE {tmp_name}{array} \
977-
USING ({column_name}::text::{tmp_name}{array})",
977+
USING ({column_name}::text{array}::{tmp_name}{array})",
978978
table_name = QuotedWithPrefix::pg_from_table_walker(column.table()),
979979
column_name = Quoted::postgres_ident(column.name()),
980980
array = array,

schema-engine/sql-migration-tests/tests/migrations/enums.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,8 @@ fn enum_array_modification_should_work(api: TestApi) {
542542
.send_sync()
543543
.assert_applied_migrations(&["01init"]);
544544

545+
api.raw_cmd(r#"INSERT INTO "Test" ("positions") VALUES (ARRAY['First', 'Second']::"Position"[])"#);
546+
545547
let dm = r#"
546548
datasource test {
547549
provider = "postgres"
@@ -623,8 +625,9 @@ fn alter_enum_and_change_default_must_work(api: TestApi) {
623625
provider = "postgres"
624626
}
625627
model Cat {
626-
id Int @id
627-
moods Mood[] @default([])
628+
id Int @id
629+
mood Mood
630+
moods Mood[] @default([])
628631
}
629632
enum Mood {
630633
SLEEPY
@@ -639,8 +642,9 @@ fn alter_enum_and_change_default_must_work(api: TestApi) {
639642
provider = "postgres"
640643
}
641644
model Cat {
642-
id Int @id
643-
moods Mood[] @default([SLEEPY])
645+
id Int @id
646+
mood Mood
647+
moods Mood[] @default([SLEEPY])
644648
}
645649
enum Mood {
646650
HUNGRY
@@ -682,7 +686,8 @@ fn alter_enum_and_change_default_must_work(api: TestApi) {
682686
BEGIN;
683687
CREATE TYPE "Mood_new" AS ENUM ('HUNGRY', 'SLEEPY');
684688
ALTER TABLE "public"."Cat" ALTER COLUMN "moods" DROP DEFAULT;
685-
ALTER TABLE "Cat" ALTER COLUMN "moods" TYPE "Mood_new"[] USING ("moods"::text::"Mood_new"[]);
689+
ALTER TABLE "Cat" ALTER COLUMN "mood" TYPE "Mood_new" USING ("mood"::text::"Mood_new");
690+
ALTER TABLE "Cat" ALTER COLUMN "moods" TYPE "Mood_new"[] USING ("moods"::text[]::"Mood_new"[]);
686691
ALTER TYPE "Mood" RENAME TO "Mood_old";
687692
ALTER TYPE "Mood_new" RENAME TO "Mood";
688693
DROP TYPE "public"."Mood_old";

0 commit comments

Comments
 (0)