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.