Ein Schema.
Jedes SQL-Backend.

Dein Schema, deine Queries, Migrationen und die reaktive Engine werden einmal geschrieben und zur Laufzeit in das dialekt-native Idiom kompiliert. Postgres ist die Referenz; MySQL, MariaDB, MSSQL und SQLite laufen denselben Code. Die Auswahl ist eine einzige Env-Variable — DB_DIALECT.

posts.entity.ts
TypeScript
// database/posts.entity.ts — one schema, any dialect
import { id, table, text, timestamp } from '@voltro/database'

export const posts = table('posts', {
  id:        id(),          // branded TypeID
  body:      text(),        // NOT NULL by default
  createdAt: timestamp(),   // timestamptz, UTC
}).reactive()

// Switch backend with one env var — the DDL, query
// builder and CDC reader all adapt:
//   DB_DIALECT=postgres   # LISTEN/NOTIFY
//   DB_DIALECT=mysql      # binlog CDC
//   DB_DIALECT=sqlite     # in-process bus

Was mit @voltro/database mitkommt.

Eine DSL, ein Query-Builder

table() + Spalten + Mixins deklarieren das Schema; ein einziger cross-dialekt Query-Builder emittiert dialektspezifisches SQL. Spalten sind standardmäßig NOT NULL; ids sind standardmäßig branded TypeIDs. Du schreibst Voltro, nicht Postgres-spezifisches Voltro.

Natives Change-Data-Capture

Reaktivität kommt aus echtem CDC: Postgres LISTEN/NOTIFY, MySQL/MariaDB Binlog (ROW-Format), MSSQL Change Tracking und ein In-Process-Bus für SQLite. Resume-Offsets werden persistiert, damit ein Neustart keine Änderung verpasst.

Migrationen, geplant & online

voltro db plan / apply mit Drift-Erkennung, Squashing, Rollback + Snapshots, Cross-Env-Sync und Zero-Downtime-Online-Änderungen — das richtige DDL für den gewählten Dialekt wird emittiert.

Reiche SQL-Oberfläche

Joins mit Eager Loading über dialekt-native JSON-Aggregation, Window-Funktionen, rekursive CTEs, DISTINCT ON, Set-Operationen, typisiertes JSON/JSONB, Arrays, Generated Columns, Volltextsuche und pgvector-Ähnlichkeit.

Read-Replicas + Read-your-Writes

Richte DB_REPLICA_URLS auf Replicas und Reads verteilen sich; pro-Subject-Read-your-Writes verfolgt LSN/GTID, sodass ein Nutzer nie älter liest als sein eigener letzter Write. (Postgres-RYW ist vollständig; MySQL/MariaDB-Wait-Mode routet heute auf Primary.)

Portabel, nicht kleinster gemeinsamer Nenner

Die DSL verbirgt die Abweichung jedes Dialekts — DDL-Emitter, JSON-agg-Compiler und FTS-Syntax wechseln mit DB_DIALECT — erreicht aber trotzdem dialekt-native Features. Turso ist ein Beta-Dialekt; Geld sind ganzzahlige Minor Units (noch kein DECIMAL-Typ).

Sechs Backends, ein mentales Modell.

Die dialektübergreifende Wette:

Postgres, MySQL, MariaDB, MSSQL und SQLite bekommen jeweils DB_DIALECT -getriebene Emission — Query-Builder, Migrations-Planer und CDC-Reader passen sich an. Postgres bleibt das empfohlene Ziel für native LISTEN/NOTIFY-Reaktivität; die anderen sind für Enterprise-Beschaffung und Embedded-Workloads da.

Für Dialekte ohne natives Cross-Instance-CDC (SQLite) oder hinter einem Load Balancer ergänze @voltro/plugin-broadcast , um Änderungen über Redis oder NATS über Replicas zu fächern.

Wie weit derselbe Code trägt.

5

SQL-Dialekte, über Paritätstests gegen echte Server geprüft statt als kompatibel angenommen

80

veröffentlichte Pakete, von denen die Dialekt-Treiber fünf austauschbare sind

Eine Schema-Änderung, beide Wege.

Eine handgeschriebene Migrationskette
Von Hand geschrieben
-- migrations/0007_add_archived.sql
ALTER TABLE posts ADD COLUMN archived boolean NOT NULL DEFAULT false;

-- migrations/0008_index_it.sql
CREATE INDEX posts_archived_idx ON posts (archived);

-- …and the same two files again, by hand, for every dialect
-- you support. Reviewing them means reading SQL to infer intent.
Die Schema-Datei IST der Sollzustand
Deklariert
// database/schema.ts — the desired state, not the diff
export const posts = table('posts', {
  body:     text(),
  archived: boolean().default(false).index(),
})

// $ voltro db plan
//   ADD COLUMN posts.archived boolean NOT NULL DEFAULT false
//   CREATE INDEX posts_archived_idx
//   0 destructive operations

Der Planer diffed die Deklaration gegen die Live-Datenbank, druckt die beabsichtigten Operationen und klassifiziert die destruktiven. Du reviewst Absicht statt Archäologie an Skripten — und dieselbe Deklaration erzeugt auf jedem unterstützten Dialekt die richtigen Statements.

Die Datenbank-Schicht im Detail.

Wie läuft eine Codebasis auf sechs verschiedenen Datenbanken?

Du beschreibst Tabellen, Spalten, Indizes und Relationen in einem TypeScript-Schema und schreibst Queries über einen Builder statt in SQL-Strings. Zur Laufzeit kompiliert der Builder in das Idiom des konfigurierten Dialekts — Postgres, MySQL, MariaDB, SQL Server, SQLite oder Turso — derselbe Anwendungscode erzeugt also auf jedem dialekt-natives SQL.

Das Backend zu wählen ist eine einzige Umgebungsvariable, DB_DIALECT, mit der Verbindung als URL oder als diskrete Felder DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE. In deinen Handlern ändert sich nichts — das macht „lokal auf SQLite entwickeln, auf Postgres deployen" zu einer Konfigurationsentscheidung statt zu einem Portierungsprojekt.

Was sich zwischen Engines wirklich unterscheidet — Identifier-Quoting, Upsert-Syntax, JSON-Zugriff, RETURNING-Klauseln, nebenläufige Index-Erstellung — ist Sache der Dialekt-Schicht. Handgeschriebenes SQL läuft über ein gemeinsames Paritäts-Harness gegen jede unterstützte Engine, ein Postgres-only-Statement scheitert also in der Testsuite statt im MySQL von jemand anderem.

Was ist eine deklarative Migration, und warum keine Migrationsdateien?

Deine Schema-Datei ist der Sollzustand. `voltro db plan` diffed sie gegen das, was in der Datenbank tatsächlich steht, und druckt die Operationen, die die Lücke schließen; `voltro db apply` fährt sie. Du schreibst kein Up und kein Down pro Änderung, und es gibt keine nummerierte Datei, deren Reihenfolge zählt.

Der Grund: eine handgeschriebene Migrationskette kodiert die Absicht zweimal — einmal im Schema, einmal in den Schritten — und diese zwei Kopien driften. Ein Differ liest den Live-Katalog und kann sich deshalb nicht darüber irren, was da ist. Er klassifiziert jede Operation, verweigert verlustbehaftete, sofern du nicht ausdrücklich zustimmst, und beweist Konvergenz: nach dem Anwenden muss der nächste Plan leer sein, sonst ist die Migration nicht fertig.

Umbenennungen werden deklariert statt erraten. `table('new', …).renamedFrom('old')` und das Spalten-Pendant sagen dem Differ, dass es ein Verschieben ist und kein Drop-und-Create — genau der Unterschied, an dem die Daten hängen. Ab einer Zeilenschwelle (Default 50.000) werden Index- und Spaltenänderungen als Online-Operationen geplant statt als tabellensperrende.

Woher kommt das Echtzeit-Verhalten?

Aus der Datenbank selbst, nicht aus einer Polling-Schleife in der Anwendung. Jeder Dialekt hat einen nativen Change-Feed — Postgres LISTEN/NOTIFY, das Binlog von MySQL und MariaDB, Change Tracking bei SQL Server, ein In-Process-Bus bei SQLite — und die Runtime konsumiert ihn. Ein Schreibvorgang von irgendwoher, auch von einem rohen SQL-Client oder einem anderen Dienst, weckt die Subscriptions, die die betroffene Tabelle lesen.

Das macht auch Cache-Invalidierung zu einer Eigenschaft des Systems statt zu einer Disziplin, die du pflegst: die Tatsache „diese Tabelle hat sich geändert" fließt ohnehin, gecachte Query-Ergebnisse werden also von demselben Event verworfen, das ein Delta in den Browser schiebt. Für Dialekte ohne instanzübergreifenden Feed — oder jedes Deployment hinter einem Load Balancer — fächert ein Broadcast-Plugin die Events über Redis oder NATS auf.

Was ist mit Replicas, Verschlüsselung und Datenresidenz?

Read-Replicas werden konfiguriert, nicht umprogrammiert: zeig DB_REPLICA_URLS auf sie, und Lesezugriffe verteilen sich, während Schreibzugriffe zum Primary gehen. Read-your-Writes-Konsistenz ist eine Einstellung — denn die Alternative, ein Nutzer, der ein Formular speichert und danach den alten Wert sieht, ist der Fehler, wegen dem Teams Replicas ganz aufgeben.

Verschlüsselung auf Spaltenebene ist ein Marker an der Spalte plus ein Schlüssel in der Umgebung; der Store verschlüsselt beim Schreiben und entschlüsselt beim Lesen, auch über den Store, den ein Plugin bekommt. Exposition ist ein separater Marker: eine server-only-Spalte wird strukturell aus jeder Wire-Antwort entfernt — „verschlüsselt" und „geht nicht an den Browser" bleiben also zwei Entscheidungen statt einer Annahme.

Datenresidenz ist deklarierbar. Konfigurier die Regionen, die du bedienst, und das Framework öffnet einen Store pro Region und routet jeden Request in die Heimatregion seines Mandanten — oder lehnt ihn ab. Das macht aus einer rechtlichen Anforderung eine Boot-Konfiguration statt einer Review-Checkliste, an die sich jemand erinnern muss.

Unterstützte Engines.

Unterstützte Datenbank-Engines und ihr Change-Capture-Mechanismus
EngineAnmerkungen
PostgreSQLDer Referenzdialekt. Natives LISTEN/NOTIFY treibt instanzübergreifende Reaktivität ohne Broker.
MySQL 8+Binlog-basiertes Change-Capture; volle Migrations- und Query-Unterstützung.
MariaDB 10.6+Binlog-basiertes Change-Capture; als eigener Dialekt behandelt, nicht als MySQL.
SQL Server 2019+Change Tracking für Reaktivität; dialektspezifisches SQL übernimmt der Compiler.
SQLite 3.38+In-Process-Change-Bus. Ein Knoten; für eine Flotte das Broadcast-Plugin ergänzen.
Turso (Beta)Der Rust-SQLite-Rewrite mit MVCC-Nebenläufigkeit. Ein Knoten; noch keine Generated Columns oder FTS.

Häufige Fragen

Kann ich auf SQLite entwickeln und auf Postgres deployen?

Ja — das ist der vorgesehene Weg. Setz DB_DIALECT lokal auf sqlite und in Produktion auf postgres; Schema, Queries, Migrationen und die reaktive Engine sind identisch. Abgesichert wird das von derselben Testsuite, die handgeschriebenes SQL über ein Paritäts-Harness gegen jeden Dialekt fährt.

Muss ich SQL-Migrationsdateien schreiben?

Nein. Die Schema-Datei ist der Sollzustand, und der Differ berechnet die Operationen dorthin. Du fährst `voltro db plan` zum Prüfen und `voltro db apply` zum Ausführen. Umbenennungen deklarierst du mit `.renamedFrom()`, damit ein Verschieben nie mit Drop-und-Create verwechselt wird.

Funktioniert Echtzeit mit mehr als einer Instanz?

Bei Postgres und MySQL/MariaDB ist der Change-Feed flottenweit — ein Schreibvorgang auf irgendeiner Replica erreicht Subscriber auf jeder Replica. Für SQLite, SQL Server oder jedes Deployment, in dem du einen expliziten Bus willst, fächert das Broadcast-Plugin die Events über Redis oder NATS auf, ohne Anwendungscode zu ändern.

Kann Voltro meine bestehende Datenbank nutzen?

Ja. Das Framework ist eine Runtime, die neben deiner Datenbank lebt — dein Connection-String, deine Backups, deine Zugriffsregeln. Bestehende Tabellen lassen sich ins Schema übernehmen, und nichts wird durch fremde Infrastruktur geleitet.

Wie werden verlustbehaftete Migrationen verhindert?

Jede geplante Operation wird klassifiziert, und eine destruktive wird ohne ausdrückliche Bestätigung verweigert. Das Anwenden beweist außerdem Konvergenz: der nächste Plan muss leer sein — eine Migration, die sich still selbst neu vorschlägt, gilt als unfertig statt als Erfolg.

Das Schema speist den Rest der Runtime.

Öffne das Framework. Schau es dir selbst an.

Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.