Kiste
DE

The book is currently only available in German.

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 KEY bei id heiß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 :titel und :jahr sind Platzhalter — die echten Werte stehen in der Karte dahinter. Warum das so wichtig ist, steht in §36.6.
  • db.letzte_id(d) verrät die id, 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 mit nimm t = z["titel"].

Achtung — länge: Nimm das eingebaute lä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 geld und dezimal deshalb als TEXT — das ist verlustfrei. Als REAL (Komma-Zahl) würden Beträge gerundet, und bei Geld ist das keine gute Idee (Kapitel 20 erklärt, warum). Beim Lesen kommt text zurü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.