diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index eb31e44..b8b5e02 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -847,6 +847,10 @@ model SearchIndex { @@id([targetType, targetId, minClearance]) @@index([targetType, minClearance]) + /// Der GIN-Index ueber den Vektor. Er muss **hier** stehen und nicht nur in + /// der Migration: sonst sieht `prisma migrate dev` einen Unterschied zur + /// Datenbank und will ihn wegraeumen. Siehe docs/13-entscheidungen.md. + @@index([vector], type: Gin) } /// Eine gespeicherte Suche. Die mitgelieferten Ansichten stehen im Programm, diff --git a/packages/db/scripts/prepare-test-db.mjs b/packages/db/scripts/prepare-test-db.mjs index 9538c42..936fa55 100644 --- a/packages/db/scripts/prepare-test-db.mjs +++ b/packages/db/scripts/prepare-test-db.mjs @@ -6,8 +6,19 @@ import { loadEnvFromRepoRoot } from '../dist/loadEnv.js' import { resolveTestDatabaseUrl } from '../dist/testDatabase.js' /** - * Legt die Testdatenbank an, falls sie fehlt, und bringt sie auf den aktuellen - * Migrationsstand. Wird von `pnpm test` vorgeschaltet. + * Setzt die Testdatenbank neu auf und bringt sie auf den Migrationsstand. + * Wird von `pnpm test` vorgeschaltet. + * + * **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und + * Protokollzeilen sind nach harter Regel 2 aus CLAUDE.md nicht löschbar — was + * ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen + * standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge + * begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche + * Fehlschläge sehen aus wie ein Fehler im Code und sind keiner; das kostet mehr + * Zeit als das Neuaufsetzen. + * + * `KEEP_TEST_DB=1` behält den Bestand. Für das schnelle Nacheinander beim + * Arbeiten an einer einzelnen Testdatei. * * Läuft nur in der Entwicklung und in der CI, nie im Betrieb. */ @@ -21,12 +32,35 @@ maintenance.pathname = '/postgres' const client = new pg.Client({ connectionString: maintenance.toString() }) await client.connect() +// Bezeichner lassen sich nicht als Parameter binden, deshalb quotiert. Der Name +// kommt aus resolveTestDatabaseUrl und endet immer auf _test. +const quoted = `"${dbName.replace(/"/g, '""')}"` + +if (!dbName.endsWith('_test')) { + throw new Error(`Sicherung: ${dbName} endet nicht auf _test, wird nicht angefasst`) +} + try { const { rowCount } = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]) - if (rowCount === 0) { - // Bezeichner lassen sich nicht als Parameter binden, deshalb quotiert. - await client.query(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`) + const behalten = process.env.KEEP_TEST_DB === '1' + + if (rowCount > 0 && !behalten) { + // Offene Verbindungen verhindern das Verwerfen — etwa ein Prisma Studio + // oder ein abgebrochener Testlauf. + await client.query( + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE datname = $1 AND pid <> pg_backend_pid()`, + [dbName], + ) + await client.query(`DROP DATABASE ${quoted}`) + console.log(`[test-db] ${dbName} verworfen`) + } + + if (rowCount === 0 || !behalten) { + await client.query(`CREATE DATABASE ${quoted}`) console.log(`[test-db] ${dbName} angelegt`) + } else { + console.log(`[test-db] ${dbName} behalten (KEEP_TEST_DB=1)`) } } finally { await client.end() diff --git a/packages/db/tests/schema-drift.test.ts b/packages/db/tests/schema-drift.test.ts new file mode 100644 index 0000000..87d4fad --- /dev/null +++ b/packages/db/tests/schema-drift.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' +import { execFileSync } from 'node:child_process' +import path from 'node:path' + +/** + * Schema und Migrationen dürfen nicht auseinanderlaufen. + * + * Der Anlass war handfest: ein von Hand angelegter GIN-Index über den + * Suchvektor stand in der Migration, aber nicht in `schema.prisma`. Prisma + * hielt das für einen Rückstand und fragte bei jedem `migrate dev` nach einem + * Namen für eine neue Migration — deren Inhalt gewesen wäre, **den Suchindex + * zu löschen**. Wer die Frage in Eile beantwortet, verliert die gestufte + * Volltextsuche und merkt es erst, wenn die Suche leer bleibt. + * + * Der Vergleich läuft gegen die Datenbank, auf die der Testlauf zeigt. Sie + * trägt alle Migrationen, also ist ein Unterschied zum Schema genau der + * Rückstand, den `migrate dev` sonst anbietet. + * + * `--exit-code`: 0 heisst kein Unterschied, 2 heisst Unterschied. + */ +const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL +const suite = url ? describe : describe.skip + +suite('Schema und Migrationen', () => { + it('haben keinen Unterschied, den migrate dev anbieten würde', () => { + const wurzel = path.resolve(__dirname, '..') + + let ausgabe = '' + let code = 0 + try { + ausgabe = execFileSync( + 'node', + [ + require.resolve('prisma/build/index.js', { paths: [wurzel] }), + 'migrate', + 'diff', + '--from-config-datasource', + '--to-schema', + path.join('prisma', 'schema.prisma'), + '--script', + '--exit-code', + ], + { cwd: wurzel, encoding: 'utf8', env: { ...process.env }, timeout: 120_000 }, + ) + } catch (error) { + const fehler = error as { status?: number; stdout?: string; stderr?: string } + code = fehler.status ?? 1 + ausgabe = `${fehler.stdout ?? ''}${fehler.stderr ?? ''}` + } + + // Die Meldung nennt das fehlende Stück, damit man nicht suchen muss. + expect(ausgabe.replace(/^Loaded Prisma config.*$/m, '').trim(), ausgabe).not.toMatch( + /^--\s*(Create|Drop|Alter|Add)/m, + ) + expect(code, ausgabe).toBe(0) + }, 130_000) +}) diff --git a/web/next-env.d.ts b/web/next-env.d.ts index 341b947..830fb59 100644 --- a/web/next-env.d.ts +++ b/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.