From 1ac7f507277be932c7ad6f1bc4e0e10932188f12 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Fri, 7 Aug 2026 22:49:56 +0200 Subject: [PATCH] fix(schema-engine): match implicit text casts PostgreSQL adds ::text to varchar partial-index predicates. Treat the normalized identifier form as equal to Prisma's generated form. Fixes prisma/prisma#29386 --- .../src/flavour/postgres/schema_differ.rs | 25 +++++++++++++++- .../create_migration_tests.rs | 29 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/schema-engine/connectors/sql-schema-connector/src/flavour/postgres/schema_differ.rs b/schema-engine/connectors/sql-schema-connector/src/flavour/postgres/schema_differ.rs index df57b35205f..4ca0533d05b 100644 --- a/schema-engine/connectors/sql-schema-connector/src/flavour/postgres/schema_differ.rs +++ b/schema-engine/connectors/sql-schema-connector/src/flavour/postgres/schema_differ.rs @@ -808,7 +808,7 @@ fn idents_semantically_eq(a: &Ident, b: &Ident) -> bool { // Compares two expressions that have already been normalized by `StripPgNormalization`. fn exprs_semantically_eq(a: &Expr, b: &Expr) -> bool { - use sqlparser::ast::CastKind; + use sqlparser::ast::{CastKind, DataType}; if a == b { return true; @@ -839,6 +839,29 @@ fn exprs_semantically_eq(a: &Expr, b: &Expr) -> bool { Expr::Value(va), ) if matches!(expr.as_ref(), Expr::Value(vb) if vb == va) => true, + // PG normalizes varchar column references with an implicit ::text cast. + ( + inner, + Expr::Cast { + kind: CastKind::DoubleColon, + expr, + data_type: DataType::Text, + .. + }, + ) + | ( + Expr::Cast { + kind: CastKind::DoubleColon, + expr, + data_type: DataType::Text, + .. + }, + inner, + ) if matches!( + expr.as_ref(), + Expr::Identifier(_) | Expr::CompoundIdentifier(_) + ) => exprs_semantically_eq(inner, expr), + // Unwrap outer :: cast when inner is also a :: cast (PG annotation wrapping user cast). ( inner, diff --git a/schema-engine/sql-migration-tests/tests/create_migration/create_migration_tests.rs b/schema-engine/sql-migration-tests/tests/create_migration/create_migration_tests.rs index 87b790ebac6..8175cf0f15f 100644 --- a/schema-engine/sql-migration-tests/tests/create_migration/create_migration_tests.rs +++ b/schema-engine/sql-migration-tests/tests/create_migration/create_migration_tests.rs @@ -398,6 +398,35 @@ fn empty_migrations_should_not_be_created(api: TestApi) { .assert_migration_directories_count(1); } +#[test_connector(tags(Postgres), exclude(CockroachDb), preview_features("partialIndexes"))] +fn partial_unique_index_on_varchar_object_predicate_is_not_recreated(mut api: TestApi) { + let dm = api.datamodel_with_provider_and_features( + r#" + model ExternalMappingData { + tenantId String @db.VarChar(255) + mappingType String @db.VarChar(255) + externalId String @db.VarChar(4096) + caralegalId String @db.VarChar(4096) + + @@id([tenantId, mappingType, externalId, caralegalId]) + @@unique([tenantId, externalId], map: "ExternalMappingData_userId_tenantId_externalId_key", where: { mappingType: "userId", externalId: { not: "" } }) + } + "#, + &[], + &["partialIndexes"], + ); + + let dir = api.create_migrations_directory(); + + api.create_migration("initial", &dm, &dir) + .send_sync() + .assert_migration_directories_count(1); + + api.create_migration("no-op", &dm, &dir) + .send_sync() + .assert_migration_directories_count(1); +} + #[test_connector] fn migration_name_length_is_validated(api: TestApi) { let dm = api.datamodel_with_provider(