diffium-db: Ein Blick auf das, was KI-Agenten mit der Datenbank anstellen

Satellitenschuessel und Antennenanlage als Silhouette vor tiefblauem Abendhimmel
Deine Reaktion:

Ein Entwickler lehnt sich zurück, während ein Agent eine Migration vorbereitet. Auf dem Bildschirm läuft ein Terminal, das jede Sekunde von der Postgres-Instanz neu liest. Links erscheinen die Unterschiede zur Baseline, rechts steht das Objekt selbst, vorher und nachher. So sieht Arbeit mit einem Sprachmodell heute aus: Man beschreibt, was man will, etwas schreibt das SQL, man liest es, gibt es frei, und Sekunden später ist das Schema ein anderes. Die aufregenden zehn Minuten danach sind das eigentliche Problem.

Was git diff nicht sehen kann

Wer mit Agenten arbeitet, kennt das Gespann aus Commit-Text und SQL-Datei. Das Versionskontrollsystem zeigt die Migrationsdatei. Es zeigt nicht, was die Migration tatsächlich getan hat. Und es zeigt schon gar nicht die Zeile, die ein Agent nebenbei aktualisiert hat, weil ihm das vernünftig erschien. Gute Schema-Diff-Tools existieren, aber sie funktionieren meist nach demselben Prinzip: Man startet sie hinterher, vergleicht zwei Datenbanken, und sie liefern ein Delta. Nichts davon bleibt während der Arbeit offen neben dem Agenten auf dem Bildschirm stehen.

Diese Lücke adressiert das Projekt diffium-db. Es handelt sich um ein Terminal-UI, das während der Arbeit geöffnet bleibt. Die Idee dahinter ist einfach, aber folgenreich: Nicht die fertige Aktion bewerten, sondern den Verlauf der Aktion am Bildschirm mitlesen. So entsteht ein Werkzeug, das für Datenbanken dieselbe Rolle spielt, die ein Datei-Diff-TUI für Quellcode schon längst spielt.

So arbeitet das Terminal-UI

Man zeigt diffium-db auf eine Postgres-Datenbank, nimmt eine Baseline, und lässt den Watcher laufen. Ab diesem Moment listet die linke Spalte alles auf, was sich seit der Baseline verändert hat. Die rechte Spalte zeigt das gewählte Objekt im Vorher-Nachher-Vergleich. Das Programm liest die Datenbank einmal pro Sekunde neu, schreibt eine Hash-Signatur für jede Zeile und vergleicht sie mit der Baseline. Wenn ein Agent eine Spalte anlegt, taucht sie in der linken Liste auf, fast so, wie eine Datei in einem Datei-Diff auftaucht.

Das Werkzeug beobachtet Tabellen mit ihren Spalten, Defaults, Identity-Spezifikationen, Constraints, Indizes und Triggern. Es beobachtet Views, materialisierte Views, Enums und Funktionen. Es beobachtet aber auch Zeilen, und genau das stellt sich als die interessantere Hälfte heraus. Wer nur Struktur prüft, sieht nur die Hälfte dessen, was passiert.

Vier Kommandos gehören zum Handwerk: `watch` öffnet das TUI, `snapshot` nimmt eine Baseline, `baselines` listet die gespeicherten Baselines, und `diff` gibt die Aenderungen aus und beendet sich. Letzteres nimmt `–exit-code` und liefert den Rückgabewert 1, sobald sich etwas geändert hat. Genug, um einen CI-Lauf scheitern zu lassen oder die Schleife eines Agenten zu stoppen, bevor er den nächsten Schritt macht.

Einrichtung und Demo

Man braucht Bun in Version 1.3 oder neuer und eine Postgres-Instanz, die überwacht werden soll. Es gibt genau eine zusätzliche Abhängigkeit, `@opentui/core`, weshalb die Installation schnell geht. Dann nimmt man eine Baseline und öffnet den Watcher. Das ist im Grunde schon die gesamte Einrichtung.

Das Repository liefert eine Demo mit, die den Ablauf sichtbar macht, ohne dass ein Agent beteiligt sein muss. Das SQL unter `examples/demo/01-baseline.sql` baut ein kleines `demo`-Schema mit Tabellen für Benutzer und Projekte auf. `examples/demo/02-agent-change.sql` simuliert, was ein Agent tut, wenn man ihm sagt: Leg Vertragspläne an, und räum etwas auf. Oeffnet man das TUI in einem Terminal und führt die zweite Datei in einem zweiten aus, landen fünf Aenderungen auf dem Bildschirm: ein neues Enum, zwei neue Spalten und ein neuer Index auf `demo.users`, ein gelöschter Index auf `demo.projects`, eine eingefügte und eine gelöschte Zeile.

Innerhalb des TUI bewegt man sich mit `j` und `k` durch die Liste, `s` wechselt zwischen nebeneinander und untereinander, `e` schreibt den vollständigen Diff in eine Datei. Im Hintergrund werkelt das OpenTUI-Framework, das eine vollständige Terminal-Oberfläche im Speicher zeichnen und damit auch ohne echtes Terminal getestet werden kann.

Warum Datenbank-Branches den Unterschied machen

Die Demo ist eine Demo. Für echte Arbeit richtet man das Werkzeug nicht auf die Produktion. Der Sinn besteht darin, den Agenten laufen zu lassen und hinterher zu lesen, was er gemacht hat, und das soll an einem Ort passieren, den man ohne Bedenken wegwerfen kann. Also geben wir dem Agenten einen Branch. Auf Neon, wo der Autor arbeitet, ist das ein Zweizeiler plus die Projekt-ID, weil dort mehrere Projekte parallel laufen und die CLI nicht raten soll, welches gemeint ist.

Man zeigt `DATABASE_URL` auf das, was der zweite Befehl ausgibt, nimmt dort die Baseline und lässt den Agenten auf dem Branch arbeiten. Der Branch entsteht Copy-on-Write aus seinem Vater, bringt also dasselbe Schema und dieselben Zeilen mit wie die Datenbank, die einen wirklich interessiert. Im vom Autor beschriebenen Beispiel dauerte das 1,2 Sekunden. Ungenutzte Branches legen ihre eigene Compute-Aktivität still, sodass vergessene Branches nicht weiter Strom verbrauchen.

Anschließend liest man den Diff. Wenn die Migration das tut, was man wollte, führt man sie gegen die echte Datenbank aus. Wenn der Agent etwas Unerwartetes getan hat, hat man das auf einer Kopie gesehen, und die Kopie löscht man. Das ist die Schleife. Sie ist billig genug, um sie jedes Mal zu durchlaufen, nicht nur, wenn man daran denkt. Prinzipiell funktioniert diffium-db mit jedem Postgres, nichts im Code ist Neon-spezifisch, aber Branching macht den Ablauf erst praktisch genug für den Alltag.

Was ein Zeilen-Diff beweisen kann

Struktur ist die einfache Hälfte. Zeilen sind die unbequeme. Um zu wissen, dass sich eine Zeile geändert hat, muss man sich merken, wie sie vorher aussah. Es ist unrealistisch, jede Zeile zu speichern, also speichert diffium-db einen Fingerprint: den Primärschlüssel, einen MD5-Hash der Textdarstellung der gesamten Zeile sowie eine kurze Vorschau. Billig in der Datenbank zu berechnen, billig zu halten, und es unterscheidet ein Insert von einem Update von einem Delete sauber.

Wenn der Primärschlüssel neu ist, handelt es sich um ein Insert. Wenn er verschwunden ist, um ein Delete. Wenn der Schlüssel gleich bleibt und der Hash sich bewegt, hat jemand diese Zeile bearbeitet. Das funktioniert, bis die Tabelle eine Spalte dazubekommt. Dann ändern sich alle Hashes auf einmal, und ein naives Werkzeug würde behaupten, jede einzelne Zeile sei bearbeitet worden. Dabei hat niemand sie bearbeitet. Die Form der Tabelle hat sich unter ihnen bewegt.

Das Werkzeug macht diesen Fehler nicht. Die Anzahl wird als `~3?` statt `~3` geschrieben, und die erste Zeile des Diffs erklärt das. Zwei Spalten sind angekommen, also liest sich jede Zeile der Tabelle als bearbeitet, und die Update-Zahl ist der Teil der Aussage, für den das Werkzeug nicht einstehen kann. Inserts und Deletes bleiben exakt, weil sie aus dem Primärschlüssel kommen und der Schlüssel sich nicht für die Form interessiert. Nur die Update-Zahl ist Raterei, und eine blanke Vermutung inmitten von Zahlen, die es beweisen kann, wäre der gesamten Anzeige abträglich. Tabellen ohne Primärschlüssel und Tabellen über `–row-limit` (standardmäßig 5000 Zeilen) bekommen nur die Zeilenzahl und keinerlei Aussage. Dasselbe Prinzip: Lieber weniger sagen als etwas, das nicht stimmt.

Warum der Struktur-Diff mit Text arbeitet

Der strukturelle Diff ruht auf einer einzelnen Entscheidung, die einen großen Teil des Verhaltens erklärt. Jedes Objekt wird in einen kanonischen Textblock mit deterministischer Zeilenreihenfolge gerendert. Eine Tabelle wird zu ihren Spalten, dann ihren Constraints, dann ihren Indizes, dann ihren Triggern, immer in genau dieser Reihenfolge. Der Struktur-Diff ist dann nichts weiter als ein Zeilen-Diff dieser Blöcke.

Diese eine Entscheidung finanziert viel. Eine hinzugefügte Spalte ist genau eine zusätzliche Zeile statt einer Tabelle, die vage anders aussieht. Nebeneinander werden die `+2 -0`-Zähler und das horizontale Scrollen durch einen langen Default-Ausdruck zum Geschenk. Es gibt einen Preis: Das Werkzeug beschreibt, wie die Datenbank jetzt aussieht, nicht das DDL-Statement, das dorthin führen würde. Für einen Watcher ist das die richtige Seite der Abwägung. Wer generierte Migrationen möchte, braucht ein anderes Programm.

Wo die Baseline gespeichert wird

Eine Baseline ist ein gespeicherter Wert, keine Live-Verbindung. Man kann sie jetzt nehmen und morgen dagegen diffen, mehrere benannte Baselines halten oder eine neben der Migration committen, die sie erzeugt hat. Per Default landen sie in `.diffium-db/snapshots/` als JSON. Mit `–store neon –store-url ` landen sie stattdessen in einem `diffium_db`-Schema in Postgres. Diese Variante nutzt man, wenn CI und Laptop sich darüber einigen müssen, was „vorher“ bedeutet.

Der Watcher schaut nie in sein eigenes Schema, also können die Store-URL und die überwachte URL dieselbe Datenbank sein. Mit dem Branch von vorhin kann die Baseline auf dem Branch selbst liegen, ein Schema versetzt von der Sache, die sie beschreibt. Der Branch ist dann das gesamte Experiment, und das Löschen des Branchs nimmt die Aufzeichnung mit.

Was noch fehlt

Was v1 nicht kann, in etwa in der Reihenfolge, in der es der Autor angehen will. Kein Bewusstsein für ORMs: v1 sagt, dass eine Spalte aufgetaucht ist. Es kann nicht sagen, welche Migrationsdatei oder welche Model-Definition sie hervorgebracht hat. Diese Zuordnung gehört zu einem ORM und soll als Nächstes kommen, der Pfad `src/orms/` ist dafür bereits angelegt und bewusst leer. Nur Postgres: Das Lesen des Katalogs lebt isoliert unter `src/pg/`, und nichts oberhalb dieser Grenze weiß, was ein Katalog ist, also hat ein zweiter Dialekt einen offensichtlichen Platz. Er existiert nur noch nicht. Keine Sequences: Jeder `bigserial` erzeugt eine, und sie bewegt sich bei jedem Insert. Reines Rauschen, bis jemand danach fragt. Polling statt Replikation: Das Werkzeug liest einmal pro Sekunde neu. Bei einer großen Datenbank wäre logische Replikation deutlich billiger, und das ist der naheliegende Schritt, falls das Polling jemals weh tut.

Eine Sache steht nicht auf dieser Liste und wird es auch nie sein. Das Werkzeug schreibt nicht in die Datenbank, die es überwacht. Das Einzige, was es schreibt, ist sein eigener Store, und das auch nur, wenn man ausdrücklich darauf zeigt.

Tests, Codeumfang und Haltung

Das Projekt umfasst rund 2.200 Zeilen Quellcode und etwa 900 Zeilen Tests. 64 dieser Tests laufen ohne jede Datenbank, einschließlich der Tests für das Terminal, weil OpenTUI in den Speicher rendert und das Gezeichnete sich prüfen lässt. Eine Aenderung am Layout bricht damit einen Test, statt still vor sich hin falsch auszusehen auf einem Screenshot, den man ohnehin nicht aktualisiert hätte.

Was den Autor nicht loslässt, ist der Zeilen-Diff. Es wäre einfacher gewesen, für jede Tabelle eine Zahl auszugeben und die Leute annehmen zu lassen, sie bedeute etwas. Die Version, die sagt „das kann ich nicht beweisen“, sieht etwas hässlicher aus. Er nimmt sie trotzdem. Ein Werkzeug, das neben einem Agenten sitzt und über dessen Arbeit berichtet, muss sorgfältiger sein als der Agent selbst. Der Code steht unter MIT-Lizenz auf GitHub. Wer es ausprobiert und es sagt etwas Falsches, soll ein Issue eröffnen.

Quelle: denislavgavrilov.com

Deine Reaktion:
Artikel teilen:
Krötzsch-Check0 — 100
Fakten 75
Relevanz 80
Hype 20
Einschätzung 75
Redaktion 50 Stand 50 · noch keine Stimmen
Ist das Hype?
Sebastian Krötzsch
Autor

Sebastian Krötzsch

Sebastian Krötzsch schreibt auf sebask.de über Künstliche Intelligenz, Automatisierung, digitale Systeme und die Frage, was davon im Alltag wirklich nützlich ist. Ohne Buzzword-Nebel, dafür mit klarem Blick auf Praxis, Tools und echte Wirkung.