Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: add tests for native SetDefault referential action (#16303)
* test: add tests for SetDefault referential action with relationMode="foreignKeys" * chore: update link in test's README * chore: fix typo in comment * Apply suggestions from code review Co-authored-by: Joël Galeran <Jolg42@users.noreply.github.com> Co-authored-by: Jan Piotrowski <piotrowski+github@gmail.com> Co-authored-by: Joël Galeran <Jolg42@users.noreply.github.com>
- Loading branch information
1 parent
3e1d6f6
commit d538d2a
Showing
7 changed files
with
593 additions
and
0 deletions.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
packages/client/tests/functional/referentialActions-setDefault/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## Testing `SetDefault` referential action (`relationMode = "foreignKeys"`) on relational databases | ||
|
||
Internal Notion pages | ||
|
||
- [Doc](https://www.notion.so/SetDefault-referential-action-on-MySQL-68d7bdbe6fc947cf8829d7ca7dc2b001) |
21 changes: 21 additions & 0 deletions
21
packages/client/tests/functional/referentialActions-setDefault/_matrix.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { defineMatrix } from '../_utils/defineMatrix' | ||
import { Providers } from '../_utils/providers' | ||
import { getProviderFromFlavor, ProviderFlavors } from '../_utils/relationMode/ProviderFlavor' | ||
|
||
const providerFlavors = [ | ||
Providers.POSTGRESQL, | ||
Providers.COCKROACHDB, | ||
Providers.SQLSERVER, | ||
Providers.SQLITE, | ||
Providers.MYSQL, // SetDefault is silently interpreted as NoAction by InnoDB on MySQL 8+ | ||
] as const | ||
|
||
const defaultUserId = 3 | ||
|
||
const providersMatrix = providerFlavors.map((providerFlavor) => ({ | ||
provider: getProviderFromFlavor(providerFlavor), | ||
providerFlavor, | ||
defaultUserId, | ||
})) | ||
|
||
export default defineMatrix(() => [providersMatrix]) |
37 changes: 37 additions & 0 deletions
37
packages/client/tests/functional/referentialActions-setDefault/prisma/_schema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { match } from 'ts-pattern' | ||
|
||
import { Providers } from '../../_utils/providers' | ||
import testMatrix from '../_matrix' | ||
import { schema_1to1 } from './_schema_1_to_1' | ||
import { schema_1ton } from './_schema_1_to_n' | ||
|
||
// Note: We can't test `SetDefault` with automatically created ids in SQLServer. | ||
// Had we defined Prisma models with automatic ids in the form of `id Int @id @default(autoincrement())`, | ||
// SQL Server would have thrown a runtime error at the `CREATE TABLE` level, as Prisma would use the IDENTITY(1, 1) type | ||
// for the id column, which cannot be created or updated explicitly. | ||
// The escape hatch would be using `SET IDENTITY_INSERT OFF`, which is however allowed only for a single table per session, | ||
// and isn't supported by Prisma (not even in raw mode, see https://github.com/prisma/prisma/issues/15305) | ||
// | ||
// The obvious solution for us was to avoid `@default(autoincrement())` in tests. | ||
// Providing a separate `id` schema definition for each provider is not necessary. | ||
export default testMatrix.setupSchema(({ provider, providerFlavor, defaultUserId }) => { | ||
const url = match({ provider, providerFlavor }) | ||
.with({ provider: Providers.SQLITE }, () => `"file:test.db"`) | ||
.otherwise(({ providerFlavor }) => `env("DATABASE_URI_${providerFlavor}")`) | ||
|
||
const schema = /* prisma */ ` | ||
generator client { | ||
provider = "prisma-client-js" | ||
} | ||
datasource db { | ||
provider = "${provider}" | ||
url = ${url} | ||
} | ||
${schema_1to1(defaultUserId)} | ||
${schema_1ton(defaultUserId)} | ||
` | ||
|
||
return schema | ||
}) |
16 changes: 16 additions & 0 deletions
16
packages/client/tests/functional/referentialActions-setDefault/prisma/_schema_1_to_1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export function schema_1to1(defaultUserId = 3) { | ||
const schema = /* prisma */ ` | ||
model UserOneToOne { | ||
id Int @id | ||
profile ProfileOneToOne? | ||
} | ||
model ProfileOneToOne { | ||
id Int @id | ||
user UserOneToOne? @relation(fields: [userId], references: [id], onUpdate: SetDefault, onDelete: SetDefault) | ||
userId Int? @default(${defaultUserId}) @unique | ||
} | ||
` | ||
|
||
return schema | ||
} |
16 changes: 16 additions & 0 deletions
16
packages/client/tests/functional/referentialActions-setDefault/prisma/_schema_1_to_n.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export function schema_1ton(defaultUserId = 3) { | ||
const schema = /* prisma */ ` | ||
model UserOneToMany { | ||
id Int @id | ||
posts PostOneToMany[] | ||
} | ||
model PostOneToMany { | ||
id Int @id | ||
user UserOneToMany? @relation(fields: [userId], references: [id], onUpdate: SetDefault, onDelete: SetDefault) | ||
userId Int? @default(${defaultUserId}) | ||
} | ||
` | ||
|
||
return schema | ||
} |
248 changes: 248 additions & 0 deletions
248
packages/client/tests/functional/referentialActions-setDefault/tests_1-to-1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
import { Providers } from '../_utils/providers' | ||
import { ConditionalError } from '../_utils/relationMode/conditionalError' | ||
import testMatrix from './_matrix' | ||
|
||
/* eslint-disable @typescript-eslint/no-unused-vars, jest/no-identical-title */ | ||
|
||
// @ts-ignore this is just for type checks | ||
declare let prisma: import('@prisma/client').PrismaClient | ||
|
||
// @ts-ignore | ||
const describeIf = (condition: boolean) => (condition ? describe : describe.skip) | ||
|
||
/** | ||
* 1:1 relation | ||
*/ | ||
testMatrix.setupTestSuite( | ||
(suiteConfig, suiteMeta) => { | ||
const conditionalError = ConditionalError.new() | ||
.with('provider', suiteConfig.provider) | ||
.with('providerFlavor', suiteConfig.providerFlavor) | ||
// @ts-ignore | ||
.with('relationMode', 'foreignKeys' as const) | ||
|
||
describe('1:n mandatory (explicit)', () => { | ||
const userModel = 'userOneToOne' | ||
const profileModel = 'ProfileOneToOne' | ||
const { defaultUserId } = suiteConfig | ||
|
||
// - create user 1 with one profile having id 1 | ||
// - create user 3 with no profile | ||
async function createTemplate() { | ||
// creating user id=1 | ||
await prisma[userModel].create({ | ||
data: { id: 1 }, | ||
}) | ||
|
||
// creating user id=${defaultUserId} | ||
await prisma[userModel].create({ | ||
data: { id: defaultUserId }, | ||
}) | ||
|
||
// creating profile id=1, userId=1 | ||
await prisma[profileModel].create({ | ||
data: { | ||
id: 1, | ||
userId: 1, | ||
}, | ||
}) | ||
} | ||
|
||
beforeEach(async () => { | ||
await prisma.$transaction([prisma[profileModel].deleteMany(), prisma[userModel].deleteMany()]) | ||
}) | ||
|
||
describe('[create]', () => { | ||
test('[create] creating a table with SetDefault is accepted', async () => { | ||
await createTemplate() | ||
|
||
const usersAndProfile = await prisma[userModel].findMany({ | ||
include: { | ||
profile: true, | ||
}, | ||
orderBy: { id: 'asc' }, | ||
}) | ||
expect(usersAndProfile).toMatchObject([ | ||
{ | ||
id: 1, | ||
profile: { | ||
id: 1, | ||
userId: 1, | ||
}, | ||
}, | ||
{ | ||
id: defaultUserId, | ||
profile: null, | ||
}, | ||
]) | ||
}) | ||
}) | ||
|
||
describe('[update]', () => { | ||
describeIf([Providers.MYSQL].includes(suiteConfig.provider))('with mysql', () => { | ||
test('[update] changing existing user id to a new one triggers NoAction under the hood', async () => { | ||
await createTemplate() | ||
|
||
await expect( | ||
prisma[userModel].update({ | ||
where: { id: 1 }, | ||
data: { | ||
id: 2, | ||
}, | ||
}), | ||
).rejects.toThrowError( | ||
conditionalError.snapshot({ | ||
foreignKeys: { | ||
[Providers.MYSQL]: 'Foreign key constraint failed on the field: `userId`', | ||
}, | ||
}), | ||
) | ||
}) | ||
}) | ||
|
||
describeIf(![Providers.MYSQL].includes(suiteConfig.provider))('without mysql', () => { | ||
test('[update] changing existing user id to a new one triggers SetDefault', async () => { | ||
await createTemplate() | ||
|
||
await prisma[userModel].update({ | ||
where: { id: 1 }, | ||
data: { | ||
id: 2, | ||
}, | ||
}) | ||
|
||
const users = await prisma[userModel].findMany({ | ||
orderBy: { id: 'asc' }, | ||
}) | ||
|
||
expect(users).toMatchObject([{ id: 2 }, { id: defaultUserId }]) | ||
|
||
const profile = await prisma[profileModel].findMany({ | ||
orderBy: { id: 'asc' }, | ||
}) | ||
|
||
expect(profile).toMatchObject([ | ||
{ | ||
id: 1, | ||
userId: defaultUserId, | ||
}, | ||
]) | ||
}) | ||
}) | ||
|
||
test('[update] removing user with default id and changing existing user id to a new one triggers SetDefault in profile, which throws', async () => { | ||
await createTemplate() | ||
|
||
await prisma[userModel].delete({ | ||
where: { id: defaultUserId }, | ||
}) | ||
|
||
// profileModel cannot fall back to { userId: defaultUserId }, as no user with that id exists | ||
await expect( | ||
prisma[userModel].update({ | ||
where: { id: 1 }, | ||
data: { | ||
id: 2, | ||
}, | ||
}), | ||
).rejects.toThrowError( | ||
conditionalError.snapshot({ | ||
foreignKeys: { | ||
[Providers.POSTGRESQL]: | ||
'Foreign key constraint failed on the field: `ProfileOneToOne_userId_fkey (index)`', | ||
[Providers.COCKROACHDB]: 'Foreign key constraint failed on the field: `(not available)`', | ||
[Providers.MYSQL]: 'Foreign key constraint failed on the field: `userId`', | ||
[Providers.SQLSERVER]: | ||
'Foreign key constraint failed on the field: `ProfileOneToOne_userId_fkey (index)`', | ||
[Providers.SQLITE]: 'Foreign key constraint failed on the field: `foreign key`', | ||
}, | ||
}), | ||
) | ||
}) | ||
}) | ||
|
||
describe('[delete]', () => { | ||
describeIf([Providers.MYSQL].includes(suiteConfig.provider))('with mysql', () => { | ||
test('[delete] changing existing user id to a new one triggers NoAction under the hood', async () => { | ||
await createTemplate() | ||
|
||
await expect( | ||
prisma[userModel].delete({ | ||
where: { id: 1 }, | ||
}), | ||
).rejects.toThrowError( | ||
conditionalError.snapshot({ | ||
foreignKeys: { | ||
[Providers.MYSQL]: 'Foreign key constraint failed on the field: `userId`', | ||
}, | ||
}), | ||
) | ||
}) | ||
}) | ||
|
||
describeIf(![Providers.MYSQL].includes(suiteConfig.provider))('without mysql', () => { | ||
test('[delete] deleting existing user one triggers SetDefault', async () => { | ||
await createTemplate() | ||
|
||
await prisma[userModel].delete({ | ||
where: { id: 1 }, | ||
}) | ||
|
||
const users = await prisma[userModel].findMany({ | ||
orderBy: { id: 'asc' }, | ||
}) | ||
|
||
expect(users).toMatchObject([{ id: defaultUserId }]) | ||
|
||
const profile = await prisma[profileModel].findMany({ | ||
include: { user: true }, | ||
orderBy: { id: 'asc' }, | ||
}) | ||
|
||
expect(profile).toMatchObject([ | ||
{ | ||
id: 1, | ||
userId: defaultUserId, | ||
}, | ||
]) | ||
}) | ||
}) | ||
|
||
test('[delete] removing user with default id and changing existing user id to a new one triggers SetDefault in profile, which throws', async () => { | ||
await createTemplate() | ||
|
||
await prisma[userModel].delete({ | ||
where: { id: defaultUserId }, | ||
}) | ||
|
||
// profileModel cannot fall back to { userId: defaultUserId }, as no user with that id exists | ||
await expect( | ||
prisma[userModel].delete({ | ||
where: { id: 1 }, | ||
}), | ||
).rejects.toThrowError( | ||
conditionalError.snapshot({ | ||
foreignKeys: { | ||
[Providers.POSTGRESQL]: | ||
'Foreign key constraint failed on the field: `ProfileOneToOne_userId_fkey (index)`', | ||
[Providers.COCKROACHDB]: 'Foreign key constraint failed on the field: `(not available)`', | ||
[Providers.MYSQL]: 'Foreign key constraint failed on the field: `userId`', | ||
[Providers.SQLSERVER]: | ||
'Foreign key constraint failed on the field: `ProfileOneToOne_userId_fkey (index)`', | ||
[Providers.SQLITE]: 'Foreign key constraint failed on the field: `foreign key`', | ||
}, | ||
}), | ||
) | ||
}) | ||
}) | ||
}) | ||
}, | ||
// Use `optOut` to opt out from testing the default selected providers | ||
// otherwise the suite will require all providers to be specified. | ||
{ | ||
optOut: { | ||
from: ['mongodb'], | ||
reason: 'Only testing relational databases using foreign keys.', | ||
}, | ||
}, | ||
) |
Oops, something went wrong.