Skip to content

Commit

Permalink
test: add tests for native SetDefault referential action (#16303)
Browse files Browse the repository at this point in the history
* 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
3 people committed Dec 21, 2022
1 parent 3e1d6f6 commit d538d2a
Show file tree
Hide file tree
Showing 7 changed files with 593 additions and 0 deletions.
@@ -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)
@@ -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])
@@ -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
})
@@ -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
}
@@ -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
}
@@ -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.',
},
},
)

0 comments on commit d538d2a

Please sign in to comment.