Standard-Bibliothek — `db`
Verfügbar ab Kiste 0.9.35. Vollständige Spec in
docs/db.md.
Bisher hast du Daten in Dateien gespeichert: als Text (Kapitel 21), als JSON (Kapitel 22) oder als CSV (Kapitel 27). Das reicht für kleine Mengen. Aber sobald du Fragen stellen willst wie „alle Werke ab 2020, nach Jahr sortiert" oder „wie viele sind es überhaupt?", wird es mühsam: Du müsstest die ganze Datei laden, alles selbst durchgehen, selbst sortieren, selbst zählen.
Dafür gibt es Datenbanken. Das db-Modul gibt dir eine — und zwar SQLite: eine vollwertige Datenbank, die als eine einzige Datei auf deiner Platte lebt. Kein Server, keine Installation, nichts einzurichten. Sie steckt schon in Kiste drin.
36.1 Tabellen, Zeilen, Spalten
Eine Datenbank besteht aus Tabellen. Eine Tabelle ist wie ein Blatt in einer Tabellenkalkulation:
| id | titel | jahr |
|---|---|---|
| 1 | Sonnenuntergang | 2026 |
| 2 | Mondaufgang | 2024 |
- Spalten (
id,titel,jahr) legen fest, welche Angaben es gibt und von welchem Typ sie sind. - Zeilen sind die einzelnen Einträge.
Mit der Datenbank redest du in einer eigenen kleinen Sprache: SQL. Die ist uralt, weit verbreitet und sieht fast wie Englisch aus — SELECT titel FROM werke heißt schlicht „nimm die Spalte titel aus der Tabelle werke". Du musst SQL hier nicht lernen; die paar Sätze in diesem Kapitel reichen für den Anfang.
Kistes db-Modul ist die Brücke: Du schickst SQL hin und bekommst Kiste-Werte zurück.
36.2 Öffnen und schließen
nutze db
nimm d = db.öffne_speicher() // Datenbank nur im Arbeitsspeicher
sag "geöffnet"
db.schließe(d)
db.öffne_speicher() gibt dir eine Datenbank, die nur im Arbeitsspeicher lebt und beim Schließen verschwindet. Perfekt zum Üben — es bleibt nichts liegen.
Für echte Daten nimmst du eine Datei:
nimm d = db.öffne("sammlung.db") // legt die Datei an, falls sie nicht existiert
Was du zurückbekommst, ist ein Handle — eine ganz-Zahl, die auf die geöffnete Datenbank verweist, wie eine Garderobenmarke. Genau wie bei bild (Kapitel 35). Diese Marke gibst du an jede weitere db-Funktion weiter.
| Funktion | Bedeutung |
|---|---|
db.öffne(pfad) |
Datenbank-Datei öffnen — legt sie an, wenn es sie nicht gibt |
db.öffne_speicher() |
Datenbank nur im Arbeitsspeicher (zum Üben und Testen) |
db.schließe(d) |
Schließen; danach ist das Handle ungültig |
36.3 Eine Tabelle anlegen und Daten einfügen
db.ausführe schickt eine Anweisung hin, die nichts zurückliefert — Tabelle anlegen, Daten einfügen, ändern, löschen. Zurück kommt die Zahl der betroffenen Zeilen.
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER)")
nimm betroffen = db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES (:titel, :jahr)",
{"titel": "Sonnenuntergang", "jahr": 2026})
sag betroffen
sag db.letzte_id(d)
db.schließe(d)
Ausgabe:
1
1
Was hier passiert:
CREATE TABLE werke (...)legt die Tabelle an.INTEGER PRIMARY KEYbeiidheißt: Die Datenbank vergibt die Nummern selbst und achtet darauf, dass jede nur einmal vorkommt.INSERT INTO … VALUES (:titel, :jahr)fügt eine Zeile ein. Die:titelund:jahrsind Platzhalter — die echten Werte stehen in der Karte dahinter. Warum das so wichtig ist, steht in §36.6.db.letzte_id(d)verrät dieid, die die Datenbank gerade vergeben hat.
36.4 Abfragen — db.abfrage
db.abfrage stellt eine Frage und liefert die Antwort als Liste von Karten: eine Karte pro Zeile, der Spaltenname ist der Schlüssel.
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER)")
db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES (:t, :j)", {"t": "Sonnenuntergang", "j": 2026})
db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES (:t, :j)", {"t": "Mondaufgang", "j": 2024})
db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES (:t, :j)", {"t": "Altes Bild", "j": 1999})
nimm zeilen = db.abfrage(d, "SELECT titel, jahr FROM werke WHERE jahr > :j ORDER BY jahr", {"j": 2000})
sag länge(zeilen)
wiederhole z in zeilen {
nimm t = z["titel"]
nimm j = z["jahr"]
sag "{t} ({j})"
}
db.schließe(d)
Ausgabe:
2
Mondaufgang (2024)
Sonnenuntergang (2026)
Das ist der Moment, in dem sich die Datenbank auszahlt: WHERE jahr > :j filtert und ORDER BY jahr sortiert — die Datenbank erledigt das, du schreibst keine einzige Schleife dafür. Du gehst am Ende nur noch durch das fertige Ergebnis.
Achtung — innere Anführungszeichen:
sag "{z["titel"]}"funktioniert nicht. Die inneren"beenden den Text vorzeitig. Zieh den Wert vorher in eine Variable — genau wie oben mitnimm t = z["titel"].
Achtung —
länge: Nimm das eingebautelänge(zeilen). Das Modul-liste.länge(Kapitel 18) nimmt eine Liste von Karten nicht an.
36.5 Genau eine Zeile, genau ein Wert
Oft willst du gar nicht alles — sondern eine Zeile oder eine einzelne Zahl. Dafür gibt es zwei Abkürzungen:
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER)")
db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES (:t, :j)", {"t": "Sonnenuntergang", "j": 2026})
// eine Zeile → eine karte
nimm z = db.eine_zeile(d, "SELECT titel, jahr FROM werke WHERE jahr = :j", {"j": 2026})
nimm t = z["titel"]
sag t
// ein einzelner Wert → praktisch für COUNT, SUM, MAX
nimm anzahl = db.ein_wert(d, "SELECT COUNT(*) FROM werke")
sag anzahl
// nichts gefunden → nichts
nimm nix = db.eine_zeile(d, "SELECT titel FROM werke WHERE jahr = :j", {"j": 1000})
sag nix
db.schließe(d)
Ausgabe:
Sonnenuntergang
1
nichts
Wenn nichts passt, bekommst du nichts zurück — nicht einen Fehler. Prüfe das Ergebnis, bevor du darauf zugreifst (Kapitel 15 zu ?. und ?? hilft dir dabei).
| Funktion | Liefert |
|---|---|
db.abfrage(d, sql[, parameter]) |
liste<karte> — alle Zeilen |
db.eine_zeile(d, sql[, parameter]) |
eine karte — oder nichts |
db.ein_wert(d, sql[, parameter]) |
den ersten Wert der ersten Zeile — oder nichts |
36.6 Parameter — klebe niemals Werte in den SQL-Text
Du könntest auf die Idee kommen, den Wert direkt in den Text zu schreiben:
// ❌ So NICHT:
db.ausführe(d, "INSERT INTO werke (titel) VALUES ('" + titel + "')")
Das ist aus zwei Gründen falsch.
Erstens bricht es bei ganz normalen Daten. Heißt das Werk Anna's Traum, steht plötzlich ein Anführungszeichen mitten im SQL — und die Anweisung ist kaputt.
Zweitens — und das ist ernst — ist es ein Sicherheitsloch. Kommt der Titel aus einem Eingabefeld, kann jemand statt eines Titels eigenes SQL eintippen. Er könnte deine ganze Tabelle löschen. Dieser Trick hat einen Namen: SQL-Injection, und er gehört zu den häufigsten Sicherheitslücken überhaupt.
Die Lösung: Platzhalter im SQL, Werte separat. Die Datenbank behandelt sie dann garantiert als Werte, niemals als Befehle. Es gibt zwei Formen.
Benannt (:name + Karte) — empfohlen, weil selbstdokumentierend:
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (titel TEXT, jahr INTEGER, bewertung REAL)")
db.ausführe(d, "INSERT INTO werke (titel, jahr, bewertung) VALUES (:titel, :jahr, :bew)",
{"titel": "Anna's Traum", "jahr": 2026, "bew": 4.5f})
nimm z = db.eine_zeile(d, "SELECT titel FROM werke WHERE jahr = :j", {"j": 2026})
nimm t = z["titel"]
sag t
db.schließe(d)
Ausgabe:
Anna's Traum
Das Anführungszeichen in Anna's Traum macht keinerlei Ärger — genau das ist der Punkt.
Positionell (? + Liste) — die Werte werden der Reihe nach eingesetzt:
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (titel TEXT, jahr INTEGER)")
db.ausführe(d, "INSERT INTO werke (titel, jahr) VALUES ('Sonne', 2026)")
nimm anzahl = db.ein_wert(d, "SELECT COUNT(*) FROM werke WHERE jahr > ?", [2000])
sag anzahl
db.schließe(d)
Ausgabe:
1
36.7 Welcher Typ wird woraus
| In der Datenbank | In Kiste |
|---|---|
INTEGER |
ganz |
REAL |
komma |
TEXT |
text |
BLOB |
bytes |
NULL |
nichts |
NULL ist der Datenbank-Ausdruck für „hier steht nichts" — in Kiste wird daraus folgerichtig nichts.
Geld: SQLite kennt keinen Geld-Typ. Kiste speichert
geldunddezimaldeshalb alsTEXT— das ist verlustfrei. AlsREAL(Komma-Zahl) würden Beträge gerundet, und bei Geld ist das keine gute Idee (Kapitel 20 erklärt, warum). Beim Lesen kommttextzurück, den du bewusst umwandelst.
36.8 Transaktionen — alles oder nichts
Stell dir eine Überweisung vor: Bei einem Konto abziehen, beim anderen draufrechnen. Was, wenn das Programm dazwischen abstürzt? Dann wäre das Geld weg — abgezogen, aber nie angekommen.
Eine Transaktion klammert mehrere Änderungen zu einer Einheit: entweder gelten alle, oder keine.
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE konto (id INTEGER PRIMARY KEY, stand INTEGER)")
db.ausführe(d, "INSERT INTO konto (id, stand) VALUES (1, 500)")
db.ausführe(d, "INSERT INTO konto (id, stand) VALUES (2, 100)")
db.beginne(d)
versuche {
db.ausführe(d, "UPDATE konto SET stand = stand - 100 WHERE id = 1")
db.ausführe(d, "UPDATE konto SET stand = stand + 100 WHERE id = 2")
db.bestätige(d)
sag "Umbuchung fertig"
} fange e {
db.rolle_zurück(d)
sag "Fehlgeschlagen — nichts wurde geändert"
}
sag db.ein_wert(d, "SELECT stand FROM konto WHERE id = 1")
sag db.ein_wert(d, "SELECT stand FROM konto WHERE id = 2")
db.schließe(d)
Ausgabe:
Umbuchung fertig
400
200
| Funktion | Bedeutung |
|---|---|
db.beginne(d) |
Transaktion starten |
db.bestätige(d) |
Alle Änderungen seit beginne endgültig übernehmen |
db.rolle_zurück(d) |
Alle Änderungen seit beginne verwerfen |
db.rolle_zurück ist dein Sicherheitsnetz: Geht irgendwo etwas schief, ist die Datenbank wieder so, wie sie vor db.beginne war.
36.9 Fehler abfangen
Jede db-Funktion wirft bei einem Fehler — du fängst sie mit versuche/fange (Kapitel 11):
nutze db
nimm d = db.öffne_speicher()
versuche {
db.abfrage(d, "SELECT * FROM gibtsnicht")
sag "unerreichbar"
} fange e {
sag "Datenbank-Fehler gefangen"
}
db.schließe(d)
Ausgabe:
Datenbank-Fehler gefangen
Typische Fälle: fehlerhaftes SQL, Tabelle existiert nicht, doppelter Primärschlüssel, oder ein Handle, das schon geschlossen wurde.
36.10 Zwei Grenzen beim kiste build
Der Tree-Walker (kiste run) ist nachsichtiger als der native Compiler (kiste build). Zwei Punkte betreffen db — beide haben einen einfachen Ausweg:
Gemischte Listen gehen nicht als Literal. ["Sonne", 2026] mischt text und ganz; das lehnt kiste build ab (Kapitel 9). Genau deshalb sind die benannten Parameter die empfohlene Form — gemischte Karten sind erlaubt:
// ❌ kiste build lehnt ab:
db.ausführe(d, "INSERT INTO w (t, j) VALUES (?, ?)", ["Sonne", 2026])
// ✅ benannt — funktioniert überall:
db.ausführe(d, "INSERT INTO w (t, j) VALUES (:t, :j)", {"t": "Sonne", "j": 2026})
nichts geht nicht als Karten-Wert. {"leer": nichts} lehnt kiste build ab. Um NULL zu schreiben, lässt du die Spalte einfach weg — sie ist dann von selbst NULL — oder schreibst NULL direkt ins SQL:
// ✅ Spalte weglassen → bleibt NULL
db.ausführe(d, "INSERT INTO w (titel) VALUES (:t)", {"t": "Ohne Jahr"})
// ✅ NULL direkt im SQL
db.ausführe(d, "UPDATE w SET jahr = NULL WHERE id = :id", {"id": 1})
36.11 Alles zusammen
nutze db
nimm d = db.öffne_speicher()
db.ausführe(d, "CREATE TABLE werke (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER, bewertung REAL)")
db.beginne(d)
db.ausführe(d, "INSERT INTO werke (titel, jahr, bewertung) VALUES (:t, :j, :b)", {"t": "Sonnenuntergang", "j": 2026, "b": 4.5f})
db.ausführe(d, "INSERT INTO werke (titel, jahr, bewertung) VALUES (:t, :j, :b)", {"t": "Mondaufgang", "j": 2024, "b": 3.5f})
db.ausführe(d, "INSERT INTO werke (titel, jahr, bewertung) VALUES (:t, :j, :b)", {"t": "Altes Bild", "j": 1999, "b": 5.0f})
db.bestätige(d)
nimm anzahl = db.ein_wert(d, "SELECT COUNT(*) FROM werke")
sag "Werke insgesamt: {anzahl}"
sag "Ab 2000:"
nimm zeilen = db.abfrage(d, "SELECT titel, jahr, bewertung FROM werke WHERE jahr > :j ORDER BY jahr", {"j": 2000})
wiederhole z in zeilen {
nimm t = z["titel"]
nimm j = z["jahr"]
nimm b = z["bewertung"]
sag " {t} ({j}) — {b} Sterne"
}
nimm beste = db.eine_zeile(d, "SELECT titel FROM werke ORDER BY bewertung DESC LIMIT 1")
nimm bt = beste["titel"]
sag "Bestes Werk: {bt}"
db.schließe(d)
Ausgabe:
Werke insgesamt: 3
Ab 2000:
Mondaufgang (2024) — 3.5 Sterne
Sonnenuntergang (2026) — 4.5 Sterne
Bestes Werk: Altes Bild
36.12 Alle db-Funktionen
| Funktion | Liefert |
|---|---|
db.öffne(pfad) |
Handle (ganz) |
db.öffne_speicher() |
Handle (ganz) |
db.schließe(d) |
— |
db.ausführe(d, sql[, parameter]) |
betroffene Zeilen (ganz) |
db.letzte_id(d) |
zuletzt vergebene id (ganz) |
db.abfrage(d, sql[, parameter]) |
liste<karte> |
db.eine_zeile(d, sql[, parameter]) |
karte oder nichts |
db.ein_wert(d, sql[, parameter]) |
ein Wert oder nichts |
db.beginne(d) |
— |
db.bestätige(d) |
— |
db.rolle_zurück(d) |
— |
Alle Funktionen können werfen — pack sie bei echten Daten in versuche/fange.