Beim Umbau einer Tabelle gehen ihre Indizes nicht mehr verloren

GEFUNDEN BEIM NACHDENKEN DARUEBER, WAS DIE NAECHSTE AUSLIEFERUNG AUF
DEM ECHTEN SERVER TUT -- nicht im Betrieb, nicht von einer Pruefung.

checkListeErweitern() baut eine Tabelle neu, wenn eine CHECK-Regel
erweitert werden muss: neue Tabelle, Daten hinueber, alte weg,
umbenennen. Sechs Aufrufe gehen durch diese Funktion. `DROP TABLE`
nimmt aber JEDEN Index der Tabelle mit, und der Bauplan aus
sqlite_master beschreibt nur die Tabelle -- die neue stand danach blank
da.

Gemerkt haette es niemand: Die Abfragen laufen weiter, sie lesen nur
die ganze Tabelle. Beim naechsten Neustart waere der Index wieder da
(er steht oben im Bauplan). "Bis zum naechsten Neustart falsch" ist
trotzdem kein Zustand, den man einbaut -- und bei einem EINDEUTIGEN
Index waere es keine Frage der Geschwindigkeit mehr, sondern der
Richtigkeit: Die Zusage "einen Kanal je Zustaendigkeit" haette bis zum
Neustart still ausgesetzt.

Beim Ausliefern der Kanaele wird `chat_raeume` genau so umgebaut. Der
Lauf sagt jetzt selbst, was er getan hat:

  'kanal' in chat_raeume.art freigeschaltet, 1 Zeilen, 7 Spalten,
  1 von 1 Indizes uebernommen, Verweise geprueft.

Und er meldet es als ACHTUNG, wenn nicht alle zurueckkommen.

DIE PRUEFUNG LAEUFT JETZT AUF EINER ALTEN DATENBANK. Bisher legte
pruef-chat-kanaele eine frische an -- dort steht 'kanal' schon im
Bauplan, die Umstellung tut nichts, und der gefaehrlichste Weg im Haus
blieb ungeprueft. Die Datenbank wird deshalb VOR dem Start in die alte
Form gebracht, mit Index und einer Zeile darin. Geprueft wird danach,
dass die Regel erweitert ist, die Spalte dazugekommen, die Zeile noch
da und der Index zurueck.

GEGENPROBE GEFAHREN: Wiederherstellung stillgelegt, Lauf wiederholt --
"FEHL der Index hat den Umbau ueberlebt (idx_chat_kanal_kategorie)".
Sie kann also auch nein sagen.

Der zweite, aeltere Umbauweg im selben Haus (die Artenumstellung fuer
'bigmatch') hat dieselbe Luecke. Er bleibt hier unangetastet: Das ist
eine dritte Abschrift derselben gefaehrlichen Prozedur, und sie ohne
eigene Pruefung anzufassen waere genau der Handgriff, der Daten
kostet. Notiert, nicht nebenbei erledigt.

pruef-chat-kanaele 73 -> 79 · pruef-spicy 60 · pruef-modi-ideen 30 ·
pruef-rollen 274.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-10 20:21:42 +02:00
co-authored by Claude Opus 5
parent d92ba76e33
commit 5f270c96c5
2 changed files with 105 additions and 1 deletions
+65
View File
@@ -53,6 +53,38 @@ process.env.PORT = "4359";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
/* ---- Die Datenbank steht ZUERST in der ALTEN Form ------------------------
WARUM DIESE PRUEFUNG NICHT AUF EINER FRISCHEN ANLAGE LAEUFT: Auf einer
frischen steht 'kanal' schon im Bauplan, und die Umstellung tut
nichts. Auf dem echten Server steht `chat_raeume` aber in der alten
Form -- dort wird die Tabelle wirklich neu gebaut, mit Sicherung,
Zeilenzaehlung und Umbenennen. Das ist der gefaehrlichste Weg im
ganzen Haus, und er war bis heute nur auf dem Papier geprueft.
Der Index hier ist kein Beiwerk: `DROP TABLE` nimmt jeden Index mit,
und der Bauplan aus sqlite_master beschreibt nur die Tabelle. Genau
das ist die Zeile, die beweisen muss, dass er zurueckkommt. */
{
const { DatabaseSync: DB } = await import("node:sqlite");
const alt = new DB(process.env.WORKSPACE_DB);
alt.exec(`
CREATE TABLE chat_raeume (
id INTEGER PRIMARY KEY AUTOINCREMENT,
art TEXT NOT NULL DEFAULT 'direkt'
CHECK (art IN ('direkt','gruppe')),
name TEXT,
erstellt TEXT NOT NULL,
erstellt_von INTEGER,
letzte_am TEXT
);
CREATE INDEX IF NOT EXISTS idx_chat_raeume_letzte ON chat_raeume (letzte_am DESC);
INSERT INTO chat_raeume (art, name, erstellt, letzte_am)
VALUES ('gruppe','Von vorher','2026-01-01T00:00:00.000Z','2026-01-01T00:00:00.000Z');
`);
alt.close();
}
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
@@ -186,6 +218,39 @@ const drei = await anmelden("modi", "CODE-TEAM-0003");
ok([dogi, hand, eins, zwei, drei].every((x) => x.ok && x.keks),
"alle fuenf sind angemeldet");
/* =======================================================================
0. Die Umstellung der alten Tabelle
======================================================================= */
console.log("");
console.log("=== Die alte Tabelle wurde umgebaut ===");
{
const plan = d.prepare(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'chat_raeume'").get()?.sql || "";
const regel = plan.match(/art\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
ok(regel.includes("'kanal'"), `die CHECK-Regel kennt jetzt 'kanal' (${regel})`);
ok(regel.includes("'direkt'") && regel.includes("'gruppe'"),
"und die beiden alten Arten stehen noch darin");
const spalten = d.prepare("PRAGMA table_info(chat_raeume)").all().map((z) => z.name);
ok(spalten.includes("kategorie"), `die Spalte kategorie ist dazugekommen (${spalten.length} Spalten)`);
/* DIE ZEILE VON VORHER. Ohne sie waere "umgebaut" auch dann gruen,
wenn die Tabelle einfach neu und leer waere. */
const alt = d.prepare("SELECT name FROM chat_raeume WHERE name = 'Von vorher'").get();
ok(!!alt, "die Zeile von vorher steht noch da");
/* UND DER INDEX. Er wurde vor dem Umbau angelegt, `DROP TABLE` hat
ihn mitgenommen, und die Umstellung muss ihn zurueckgebracht
haben. */
const idx = d.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'chat_raeume'")
.all().map((z) => z.name);
ok(idx.includes("idx_chat_raeume_letzte"),
`der Index hat den Umbau ueberlebt (${idx.join(", ") || "keiner"})`);
ok(idx.includes("idx_chat_kanal_kategorie"),
"und der neue eindeutige Index steht auch da");
}
/* =======================================================================
1. Einen Kanal anlegen -- und wer das darf
======================================================================= */