Validation fails to detect invalid `SetNull` referential action referencing non-optional fields
See original GitHub issueThis missed validation triggers a migration error when using MySQL, SQL Server, SQLite, and CockroachDB, but not on Postgres.
Example with MySQL:
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URI_MYSQL")
}
model User {
id String @id
profile Profile?
}
model Profile {
id String @id
user User @relation(fields: [userId], references: [id], onUpdate: SetNull, onDelete: SetNull)
// notice that this field should become optional in order to support `SetNull`
userId String @unique
}
We can see that the schema is wrongfully considered valid:
❯ prisma validate
Prisma schema loaded from prisma/schema.prisma
The schema at /.../reprod/prisma/schema.prisma is valid 🚀
If we attempt a push, we get the following error:
❯ prisma db push --skip-generate
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "PRISMA_DB_NAME" at "localhost:3306"
MySQL database PRISMA_DB_NAME created at localhost:3306
Error: Column 'userId' cannot be NOT NULL: needed in a foreign key constraint 'Profile_userId_fkey' SET NULL
0: sql_migration_connector::apply_migration::migration_step
with step=AddForeignKey { foreign_key_id: ForeignKeyId(0) }
at migration-engine/connectors/sql-migration-connector/src/apply_migration.rs:21
1: sql_migration_connector::apply_migration::apply_migration
at migration-engine/connectors/sql-migration-connector/src/apply_migration.rs:10
2: migration_core::state::SchemaPush
at migration-engine/core/src/state.rs:384
If we try to create/update the Profile
model via the Prisma client, we get the following migration error:
await prisma.$transaction([
prisma.user.create({
data: {
id: '1'.
profile: {
create: { id }
}
}
})
])
Column 'userId' cannot be NOT NULL: needed in a foreign key constraint 'Profile_userId_fkey' SET NULL
0: sql_migration_connector::apply_migration::apply_migration
at migration-engine/connectors/sql-migration-connector/src/apply_migration.rs:10
1: migration_core::state::SchemaPush
at migration-engine/core/src/state.rs:384
If we exclude the onUpdate
/onDelete
referential actions, the schema above generates the following SQL
statements (we can check that via prisma migrate dev
):
-- CreateTable
CREATE TABLE `User` (
`id` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Profile` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
UNIQUE INDEX `Profile_userId_key`(`userId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Profile` ADD CONSTRAINT `Profile_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`);
On Postgres, oddly, prisma db push
it doesn’t fail, so migration error is thrown:
❯ prisma db push --skip-generate
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "PRISMA_DB_NAME", schema "public" at "localhost:5432"
PostgreSQL database PRISMA_DB_NAME created at localhost:5432
🚀 Your database is now in sync with your Prisma schema. Done in 64ms
SetNull
on Valid Schema
If we want prisma db push
to work on other databases like mysql:8
when using the SetNull
referential action, we’d need to change the schema above as:
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("TEST_MYSQL_URI_MIGRATE")
referentialIntegrity = "foreignKeys"
}
model User {
id String @id
profile Profile?
}
model Profile {
id String @id
user User? @relation(fields: [userId], references: [id], onUpdate: SetNull, onDelete: SetNull)
userId String? @unique
}
which generates the following SQL
statements:
-- CreateTable
CREATE TABLE `User` (
`id` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Profile` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NULL,
UNIQUE INDEX `Profile_userId_key`(`userId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Profile` ADD CONSTRAINT `Profile_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE SET NULL;
Issue Analytics
- State:
- Created a year ago
- Comments:7 (7 by maintainers)
Top GitHub Comments
Makes sense. One thing to keep in mind that could prevent us from going into that direction is the principle that we should be able to introspect a valid database schema to a valid prisma schema in as many cases as possible, so if it’s valid at the database level, we may want to accept it. Maybe warn.
To summarise the output of the tests done in https://github.com/prisma/prisma/pull/15728 relevant to this issue:
postgres
doesn’t validate DDL statements that set aSET NULL
referential action on a foreign key constraint whose referenced column (or one of the referenced columns) isNOT NULL
, all other database throw a validation error in this case.postgres
fails at runtime with a not-null constraint violation error whenUPDATE
/DELETE
statements trigger theSET NULL
referential action on a column defined asNOT NULL
Optional but nice DX: There’s also an odd behavior that may be useful to document and/or validate against:
sqlserver
fails at runtime on 1:1 NULL relations becauseNULL
conflicts withUNIQUE
indexes in that particular database