From 4f85f65ab2f2c24afa3b077a4179fde0c93efa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Galeran?= Date: Thu, 24 Nov 2022 12:32:45 +0100 Subject: [PATCH] test(client): reproduction for #16423 m:n dangling pivot table entry after deleting (#16428) * test(client): reproduction for #16423 m:n dangling pivot table entry after deleting Closes https://github.com/prisma/prisma/issues/16423 * Update packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/tests.ts [skip ci] --- .../_matrix.ts | 23 ++ .../prisma/_schema.ts | 35 +++ .../tests.ts | 270 ++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/_matrix.ts create mode 100644 packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/prisma/_schema.ts create mode 100644 packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/tests.ts diff --git a/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/_matrix.ts b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/_matrix.ts new file mode 100644 index 000000000000..354894ec1be2 --- /dev/null +++ b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/_matrix.ts @@ -0,0 +1,23 @@ +import { defineMatrix } from '../../_utils/defineMatrix' +import { Providers } from '../../_utils/providers' + +export default defineMatrix(() => [ + [ + { + provider: Providers.POSTGRESQL, + relationMode: 'prisma', + }, + { + provider: Providers.POSTGRESQL, + relationMode: '', // empty means default (foreignKeys) + }, + { + provider: Providers.MYSQL, + relationMode: 'prisma', + }, + { + provider: Providers.MYSQL, + relationMode: '', // empty means default (foreignKeys) + }, + ], +]) diff --git a/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/prisma/_schema.ts b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/prisma/_schema.ts new file mode 100644 index 000000000000..b49199e1d49b --- /dev/null +++ b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/prisma/_schema.ts @@ -0,0 +1,35 @@ +import testMatrix from '../_matrix' + +export default testMatrix.setupSchema(({ provider, relationMode }) => { + // if relationMode is not defined, we do not add the line + const relationModeLine = relationMode ? `relationMode = "${relationMode}"` : '' + + const schemaHeader = /* Prisma */ ` +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "${provider}" + url = env("DATABASE_URI_${provider}") + ${relationModeLine} +} + ` + return /* Prisma */ ` +${schemaHeader} + +model Item { + id Int @id @default(autoincrement()) + categories Category[] + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt +} + +model Category { + id Int @id @default(autoincrement()) + items Item[] + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt +} + ` +}) diff --git a/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/tests.ts b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/tests.ts new file mode 100644 index 000000000000..98be68db7fba --- /dev/null +++ b/packages/client/tests/functional/issues/16390-relation-mode-m-n-dangling-pivot/tests.ts @@ -0,0 +1,270 @@ +import testMatrix from './_matrix' + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// @ts-ignore this is just for type checks +declare let prisma: import('@prisma/client').PrismaClient + +testMatrix.setupTestSuite( + (suiteConfig, suiteMeta) => { + describe('issue 16390', () => { + afterEach(async () => { + // Start from a clean state + await prisma.item.deleteMany({}) + await prisma.category.deleteMany({}) + if (suiteConfig['provider'] === 'mysql') { + await prisma.$executeRaw`TRUNCATE TABLE \`_CategoryToItem\`;` + } else { + await prisma.$executeRaw`TRUNCATE TABLE "_CategoryToItem";` + } + }) + + afterAll(async () => { + await prisma.$disconnect() + }) + + test('when deleting an item, the corresponding entry in the implicit pivot table should be deleted', async () => { + // Create one category + const category = await prisma.category.create({ + data: {}, + }) + expect(category).toMatchObject({ + id: 1, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Create one item linked to the category + const item = await prisma.item.create({ + data: { + categories: { + connect: { + id: category.id, + }, + }, + }, + include: { + categories: true, + }, + }) + expect(item).toMatchObject({ + categories: [{ id: 1, createdAt: expect.any(Date), updatedAt: expect.any(Date) }], + id: 1, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Check the pivot table entries + let pivotTable + if (suiteConfig['provider'] === 'mysql') { + pivotTable = await prisma.$queryRaw`SELECT * FROM \`_CategoryToItem\`;` + } else { + pivotTable = await prisma.$queryRaw`SELECT * FROM "_CategoryToItem";` + } + expect(pivotTable).toMatchInlineSnapshot(` + Array [ + Object { + A: 1, + B: 1, + }, + ] + `) + + // Delete the item + expect( + await prisma.item.delete({ + where: { + id: item.id, + }, + }), + ).toMatchObject({ + id: 1, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Item query now returns null + expect( + await prisma.item.findUnique({ + where: { + id: item.id, + }, + include: { + categories: true, + }, + }), + ).toBeNull() + + // Category has no items + expect( + await prisma.category.findUnique({ + where: { + id: category.id, + }, + include: { + items: true, + }, + }), + ).toMatchObject({ + id: 1, + items: [], + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Everything looks good but.... + // Let's check the pivot table + + // Check the pivot table entries + let pivotTableAfterDelete + if (suiteConfig['provider'] === 'mysql') { + pivotTableAfterDelete = await prisma.$queryRaw`SELECT * FROM \`_CategoryToItem\`;` + } else { + pivotTableAfterDelete = await prisma.$queryRaw`SELECT * FROM "_CategoryToItem";` + } + + // ... the pivot table entry is still there! + // This is a bug in the relationMode="prisma" emulation + if (suiteConfig.relationMode === 'prisma') { + expect(pivotTableAfterDelete).toStrictEqual([ + { + A: 1, + B: 1, + }, + ]) + } else { + // This is the expected behavior for prisma and foreignKeys + // once this bug is fixed + expect(pivotTableAfterDelete).toStrictEqual([]) + } + }) + }) + + test('when deleting a category, the corresponding entry in the implicit pivot table should be deleted', async () => { + // Create one category + const category = await prisma.category.create({ + data: {}, + }) + expect(category).toMatchObject({ + id: 2, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Create one item linked to the category + const item = await prisma.item.create({ + data: { + categories: { + connect: { + id: category.id, + }, + }, + }, + include: { + categories: true, + }, + }) + expect(item).toMatchObject({ + categories: [{ id: 2, createdAt: expect.any(Date), updatedAt: expect.any(Date) }], + id: 2, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Check the pivot table entries + let pivotTable + if (suiteConfig['provider'] === 'mysql') { + pivotTable = await prisma.$queryRaw`SELECT * FROM \`_CategoryToItem\`;` + } else { + pivotTable = await prisma.$queryRaw`SELECT * FROM "_CategoryToItem";` + } + expect(pivotTable).toMatchInlineSnapshot(` + Array [ + Object { + A: 2, + B: 2, + }, + ] + `) + + // Delete the category + expect( + await prisma.category.delete({ + where: { + id: item.id, + }, + }), + ).toMatchObject({ + id: 2, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Category query now returns null + expect( + await prisma.category.findUnique({ + where: { + id: category.id, + }, + include: { + items: true, + }, + }), + ).toBeNull() + + // Item has no category + expect( + await prisma.item.findUnique({ + where: { + id: item.id, + }, + include: { + categories: true, + }, + }), + ).toMatchObject({ + id: 2, + categories: [], + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }) + + // Everything looks good but.... + // Let's check the pivot table + + // Check the pivot table entries + let pivotTableAfterDelete + if (suiteConfig['provider'] === 'mysql') { + pivotTableAfterDelete = await prisma.$queryRaw`SELECT * FROM \`_CategoryToItem\`;` + } else { + pivotTableAfterDelete = await prisma.$queryRaw`SELECT * FROM "_CategoryToItem";` + } + + // ... the pivot table entry is still there! + // This is a bug in the relationMode="prisma" emulation + // + // TODO once the bug is fixed: remove conditional + // pivot table should be empty + if (suiteConfig.relationMode === 'prisma') { + expect(pivotTableAfterDelete).toStrictEqual([ + { + A: 2, + B: 2, + }, + ]) + } else { + // This is the expected behavior for prisma and foreignKeys + // once this bug is fixed + expect(pivotTableAfterDelete).toStrictEqual([]) + } + }) + }, + // Use `optOut` to opt out from testing the default selected providers + // otherwise the suite will require all providers to be specified. + { + optOut: { + from: ['sqlite', 'mongodb', 'cockroachdb', 'sqlserver'], + reason: 'Only testing postgresql and mysql', + }, + }, +)