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
======================================================================= */
+40 -1
View File
@@ -799,6 +799,32 @@ function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) {
}
const liste = spalten.map((n) => `"${n}"`).join(", ");
/* DIE INDIZES GEHEN MIT -- SONST GEHEN SIE VERLOREN (10.09.2026).
`DROP TABLE` nimmt jeden Index der Tabelle mit. Der Bauplan aus
sqlite_master beschreibt nur die TABELLE, nicht ihre Indizes; die
neue stand danach blank da.
Aufgefallen ist das nicht im Betrieb, sondern beim Nachdenken
darueber, was diese Auslieferung auf dem echten Server tut: Dort
wird `chat_raeume` umgebaut, und `idx_chat_raeume_letzte` waere
danach weg gewesen. Gemerkt haette es niemand -- die Abfrage laeuft
weiter, sie liest nur die ganze Tabelle. Beim naechsten Neustart
stuende der Index wieder da (er steht oben im Bauplan), aber "bis
zum naechsten Neustart falsch" ist kein Zustand, den man einbaut.
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.
`sql IS NULL` filtert die Indizes weg, die SQLite sich selbst zu
einer UNIQUE-Spalte baut: Die entstehen mit der neuen Tabelle von
allein, und ihr Name (`sqlite_autoindex_...`) ist gar nicht
anlegbar. */
const indizes = d.prepare(
"SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL")
.all(tabelle).map((z) => z.sql);
/* Fremdschluessel muessen aus sein, weil andere Tabellen auf
personen(id) zeigen -- und das laesst sich nicht innerhalb einer
Transaktion umschalten. */
@@ -819,13 +845,26 @@ function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) {
d.exec(`DROP TABLE ${tabelle};`);
d.exec(`ALTER TABLE ${tabelle}_neu RENAME TO ${tabelle};`);
d.exec("COMMIT");
/* ERST NACH DEM COMMIT. Waere zurueckgerollt worden, stuenden die
alten Indizes noch -- ein zweites Anlegen waere dann ein Fehler
ohne Anlass. */
let indizesZurueck = 0;
for (const bau of indizes) {
try { d.exec(bau); indizesZurueck++; }
catch (f) { console.error("[workspace] Index nach der Umstellung:", f?.message); }
}
if (indizesZurueck !== indizes.length) {
console.error(`[workspace] ACHTUNG: nur ${indizesZurueck} von ${indizes.length} `
+ `Indizes auf ${tabelle} wiederhergestellt. Sicherung: ${sicherung}`);
}
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
if (kaputt.length) {
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
"verwaiste Verweise. Sicherung liegt unter", sicherung);
} else {
console.log(`[workspace] '${marker}' in ${tabelle}.${spalte} freigeschaltet, `
+ `${nachher} Zeilen, ${spalten.length} Spalten uebernommen, Verweise geprueft.`);
+ `${nachher} Zeilen, ${spalten.length} Spalten, ${indizesZurueck} von `
+ `${indizes.length} Indizes uebernommen, Verweise geprueft.`);
}
}
} catch (fehler) {