Turso / libSQL Patterns
Kurzanleitung: Verwenden Sie
@libsql/clientfür den gesamten Turso-Datenbankzugriff. Nutzen Sieexecute()für einzelne Abfragen,batch()für atomare Multi-Statement-Operationen (bevorzugt gegenüber interaktiven Transaktionen) undtransaction()nur dann, wenn nachfolgende Abfragen von vorherigen Ergebnissen abhängen. Für Edge-/Serverless-Runtimes ohne Dateisystemzugriff importieren Sie von@libsql/client/web. Konfigurieren Sie für Lesevorgänge ohne Latenz eingebettete Replikate mit einer lokalen Datei-URL +syncUrl. Alle Schreibvorgänge werden an die primäre Datenbank weitergeleitet – planen Sie für 15–50 ms Schreiblatenz. Turso ist unter der Haube SQLite: Single-Writer-Modell, keinALTER TABLE ... ADD CONSTRAINT, keine gespeicherten Prozeduren.
<critical_requirements>
KRITISCH: Vor der Nutzung dieses Skills
Der gesamte Code muss den Projektkonventionen in CLAUDE.md folgen (kebab-case, benannte Exporte, Import-Reihenfolge,
import type, benannte Konstanten)
(Sie MÜSSEN batch() mit einem Transaktionsmodus für atomare Multi-Statement-Operationen verwenden – es ist schneller und sicherer als interaktives transaction(), da es in einem einzigen Roundtrip ausgeführt wird)
(Sie MÜSSEN in Edge-/Serverless-Runtimes ohne Dateisystemzugriff (Cloudflare Workers, Vercel Edge Functions) von @libsql/client/web importieren – der Basissimport @libsql/client zieht native Bindungen, die in diesen Umgebungen fehlschlagen)
(Sie MÜSSEN einen Transaktionsmodus ("write", "read" oder "deferred") als zweites Argument für batch() und transaction() angeben – der Standard ist "deferred", der bei INSERT/UPDATE/DELETE stillschweigend keinen Schreib-Lock erwirbt)
(Sie MÜSSEN client.close() aufrufen, wenn der Client in kurzlebigen Prozessen nicht mehr benötigt wird – offene Clients halten Verbindungen und Dateihandles)
(Sie DÜRFEN NICHT direkt auf die lokale eingebettete Replikat-Datenbankdatei zugreifen, während der Client läuft – gleichzeitiger Zugriff führt zu Datenkorruption)
</critical_requirements>
Auto-Erkennung: Turso, libSQL, @libsql/client, createClient, turso.io, embedded replica, syncUrl, syncInterval, turso db, turso group, libsql, .turso.io, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN
Wann verwenden:
- Abfragen einer von Turso gehosteten SQLite-Datenbank aus beliebigen Runtimes (Node.js, Edge, Serverless)
- Einrichten von eingebetteten Replikaten für latenzfreie lokale Lesevorgänge, die von einem entfernten Primärserver synchronisiert werden
- Multi-Tenant-SaaS mit einer Datenbank pro Mandant (Turso unterstützt Millionen von Datenbanken)
- Serverless-/Edge-Funktionen, die eine Datenbank ohne Connection-Pooling-Komplexität benötigen
- Ausführen von atomaren Multi-Statement-Operationen mit
batch()oder interaktivemtransaction() - Verwalten von Datenbankgruppen und Multi-Region-Platzierung über die Turso-CLI
Wann NICHT verwenden:
- Schreibintensive Workloads, die starke Multi-Writer-Konsistenz erfordern (Turso ist Single-Writer, Schreibvorgänge werden an den Primärserver weitergeleitet)
- Komplexe relationale Abfragen, die PostgreSQL-Funktionen benötigen (CTEs mit mutierenden Unterabfragen, gespeicherte Prozeduren, erweiterte Constraints)
- Komplexe verteilte Transaktionen über mehrere Datenbanken hinweg
- Große analytische Datensätze (SQLite-Zeilenbegrenzungen und Parallelitätsbeschränkungen gelten)
Detaillierte Ressourcen:
- examples/core.md – Client-Einrichtung, execute, batch, Transaktionen, Importpfade
- examples/embedded-replicas.md – Lokale Replikate, Synchronisation, Offline-Modus, Verschlüsselung
- reference.md – Entscheidungsrahmen, Typdefinitionen, CLI-Befehle, Nachschlagetabellen
Philosophie
Turso bringt SQLite an den Edge, indem es libSQL (einen Fork von SQLite) als verwalteten Dienst mit Multi-Region-Replikation hostet. Der @libsql/client-Treiber bietet eine einheitliche API, die identisch funktioniert, egal ob Sie eine Verbindung zu einer entfernten Turso-Datenbank, einer lokalen SQLite-Datei, einer In-Memory-Datenbank oder einem eingebetteten Replikat herstellen, das von einem entfernten Primärserver synchronisiert wird.
Kernprinzipien:
- Batch vor Transaktion –
batch()sendet alle Anweisungen in einem einzigen Roundtrip und führt sie in einer impliziten Transaktion aus. Interaktivestransaction()erfordert mehrere Roundtrips und hält einen Datenbank-Lock (5-Sekunden-Timeout). Verwenden Siebatch(), es sei denn, Sie benötigen bedingte Logik zwischen Abfragen. - Schreibvorgänge treffen immer den Primärserver – Selbst bei eingebetteten Replikaten werden Schreibvorgänge an die entfernte primäre Datenbank weitergeleitet. Die Schreiblatenz beträgt 15–50 ms, abhängig von der Entfernung zur Primärregion. Planen Sie entsprechend: optimistische Benutzeroberfläche, Hintergrundsynchronisation, vermeiden Sie schreibintensive Hot Paths.
- Eingebettete Replikate für Lesevorgänge – Eine lokale SQLite-Datei, die vom entfernten Primärserver synchronisiert wird. Lesevorgänge sind im Mikrosekundenbereich. Schreibvorgänge werden an den entfernten Server weitergeleitet. Die lokale Datei wird nach einem erfolgreichen Schreibvorgang aktualisiert (Read-your-Writes-Semantik).
- Zwei Importpfade –
@libsql/cliententhält native SQLite-Bindungen für Node.js und unterstütztfile:-URLs.@libsql/client/webist reines JS/WASM für Edge-Runtimes (Cloudflare Workers, Vercel Edge Functions) und kann keine lokalen Dateien öffnen. - SQLite-Semantik – Turso ist SQLite. Kein
ADD CONSTRAINT, keine gespeicherten Prozeduren, keinLISTEN/NOTIFY, Single-Writer-WAL-Modus. Kennen Sie die Einschränkungen von SQLite, bevor Sie sich für Turso entscheiden.
Kernmuster
Muster 1: Client-Einrichtung
Erstellen Sie einen Client mit createClient(). Die url bestimmt den Verbindungstyp: libsql:// für entfernte Turso-, file: für lokale SQLite (nur Node.js), :memory: für In-Memory (Tests). Verwenden Sie immer Umgebungsvariablen für authToken – kodieren Sie Anmeldeinformationen niemals fest.
Vollständige Einrichtungsmuster inklusive Singleton-Module und schlechter Beispiele finden Sie in examples/core.md.
Muster 2: Abfragen ausführen
execute() führt eine einzelne SQL-Anweisung aus. Verwenden Sie immer parametrisierte Abfragen mit args – niemals String-Interpolation.
// Positional: args als Array
await client.execute({
sql: "SELECT * FROM users WHERE id = ?",
args: [userId],
});
// Benannt: args als Objekt (nackte Namen entsprechen :name, @name, $name in SQL)
await client.execute({
sql: "INSERT INTO users (name, email) VALUES (:name, :email)",
args: { name, email },
});
Gibt ResultSet zurück mit rows (Array), columns, rowsAffected, lastInsertRowid (bigint). Siehe examples/core.md für typisierte Ergebniszuordnung und schlechte Beispiele.
Muster 3: Batch-Operationen
batch() führt mehrere Anweisungen atomar in einem einzigen Roundtrip aus. Alle erfolgreich oder alle zurückgerollt. Geben Sie immer den Transaktionsmodus als zweites Argument an.
const results = await client.batch(
[
{ sql: "INSERT INTO users (name) VALUES (?)", args: ["Alice"] },
{
sql: "INSERT INTO audit_log (action, entity_id) VALUES (?, last_insert_rowid())",
args: ["user_created"],
},
],
"write", // Erforderlich für INSERT/UPDATE/DELETE
);
Verwenden Sie "write" für Mutationen, "read" für reine SELECT-Abfragen (ermöglicht parallele Ausführung), "deferred" zum Starten als schreibgeschützt und Eskalieren bei Bedarf. last_insert_rowid() funktioniert über Anweisungen im selben Batch hinweg. Siehe examples/core.md für Multi-Insert- und Read-Only-Batch-Muster und reference.md für die Vergleichstabelle der Transaktionsmodi.
Muster 4: Interaktive Transaktionen
Verwenden Sie transaction() nur, wenn nachfolgende Abfragen von Ergebnissen vorheriger Abfragen abhängen. Es hält einen Datenbank-Lock (5-Sekunden-Idle-Timeout) und erfordert mehrere Roundtrips. Verwenden Sie immer try/catch/finally mit close().
const tx = await client.transaction("write");
try {
const { rows } = await tx.execute({
sql: "SELECT balance FROM accounts WHERE id = ?",
args: [fromId],
});
// ... bedingte Logik basierend auf Ergebnissen ...
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
} finally {
tx.close();
}
Wenn alle Anweisungen im Voraus bekannt sind und keine bedingte Logik erforderlich ist, verwenden Sie stattdessen batch(). Siehe examples/core.md für ein vollständiges Beispiel zum Kauf mit Bestandsprüfung.
Muster 5: Importpfade für verschiedene Runtimes
// Node.js, Bun, Deno (hat Dateisystemzugriff, native Bindungen)
import { createClient } from "@libsql/client";
// Edge-/Serverless-Runtimes OHNE Dateisystem (Cloudflare Workers, Vercel Edge)
import { createClient } from "@libsql/client/web";
Der Basissimport @libsql/client bündelt native SQLite-Bindungen, die in Edge-Runtimes fehlschlagen. @libsql/client/web ist reines JS/WASM, kann aber keine lokalen file:-URLs öffnen. Siehe examples/core.md für ein vollständiges Cloudflare-Worker-Beispiel.
Muster 6: Eingebettete Replikate
Eine lokale SQLite-Datei, die von einem entfernten Turso-Primärserver synchronisiert wird. Lesevorgänge sind lokal (Mikrosekunden), Schreibvorgänge werden an den entfernten Server weitergeleitet (15–50 ms).
const client = createClient({
url: "file:local-replica.db",
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60, // Auto-Synchronisation alle 60 Sekunden
});
await client.sync(); // Lokales Replikat vor dem ersten Lesevorgang befüllen
Wichtige Punkte: file:-URL für das lokale Replikat, syncUrl für den entfernten Primärserver, sync() beim Start aufrufen, Lesevorgänge sind lokal, Schreibvorgänge werden an den entfernten Server weitergeleitet mit Read-your-Writes-Semantik.
Wann verwenden: VMs, VPS, Container oder jeder langlebige Prozess mit Dateisystemzugriff. Nicht verfügbar in serverlosen/Edge-Runtimes ohne Dateisystem. Siehe examples/embedded-replicas.md für vollständige Muster inklusive manueller Synchronisation, Offline-Modus und Verschlüsselung.
Muster 7: Datenbankgruppen und Multi-Region
Turso organisiert Datenbanken in Gruppen. Jede Gruppe hat eine Primärregion und optionale Replikatstandorte. Alle Datenbanken in einer Gruppe erben deren Standorte.
Alle Schreibvorgänge gehen an die Primärregion, unabhängig davon, welches Replikat den Lesevorgang bearbeitet. Das Hinzufügen weiterer Standorte verbessert die Leselatenz weltweit, reduziert jedoch nicht die Schreiblatenz. Die Schreiblatenz wird durch die Entfernung zur Primärregion bestimmt.
Siehe reference.md für die vollständige Referenz der Turso-CLI-Befehle (turso db create, turso group create, turso db shell usw.).
<decision_framework>
Entscheidungsrahmen
batch() vs. transaction()
Sind alle SQL-Anweisungen im Voraus bekannt (keine bedingte Logik dazwischen)?
+-- JA --> Verwenden Sie batch() (einzelner Roundtrip, implizite Transaktion, bevorzugt)
+-- NEIN --> Hängen spätere Anweisungen von Ergebnissen früherer Anweisungen ab?
+-- JA --> Verwenden Sie transaction() (interaktiv, mehrere Roundtrips, hält Lock)
+-- NEIN --> Verwenden Sie batch()
Auswahl des Importpfads
Welche Laufzeitumgebung?
+-- Node.js / Bun / Deno / VM / Container
| +-- Benötigen Sie eingebettete Replikate (lokale Datei)?
| | +-- JA --> @libsql/client mit file:-URL + syncUrl
| | +-- NEIN --> @libsql/client mit libsql://-URL
+-- Cloudflare Workers / Vercel Edge / Browser / Serverless ohne Dateisystem
+-- @libsql/client/web (nur Remote-Verbindungen, keine file:-URLs)
Eingebettetes Replikat vs. Nur-Remote
Ist der Prozess langlebig mit Dateisystemzugriff?
+-- JA --> Benötigen Sie Lesevorgänge im Sub-Millisekunden-Bereich?
| +-- JA --> Eingebettetes Replikat (file:-URL + syncUrl)
| +-- NEIN --> Nur-Remote ist einfacher (libsql://-URL)
+-- NEIN (Serverless, Edge, kurzlebig)
+-- Nur-Remote (@libsql/client/web, libsql://-URL)
Auswahl des Transaktionsmodus
Welche Operationen führt der Batch/die Transaktion aus?
+-- Nur SELECT-Abfragen --> "read" (ermöglicht parallele Ausführung auf Replikaten)
+-- Irgendwelche INSERT / UPDATE / DELETE --> "write" (erwirbt exklusiven Lock)
+-- Unsicher zum Zeitpunkt des Aufrufs --> "deferred" (startet als schreibgeschützt, eskaliert bei Bedarf)
</decision_framework>
<red_flags>
ROTE FLAGGEN
Probleme mit hoher Priorität:
- Fehlender Transaktionsmodus in
batch()/transaction()– Das Weglassen des zweiten Arguments standardisiert auf"deferred", das als schreibgeschützt startet und möglicherweise stillschweigend keinen Schreib-Lock für INSERT/UPDATE/DELETE erwirbt. Geben Sie den Modus immer explizit an. - Verwendung von
@libsql/clientin Edge-Runtimes – Das Basispaket bündelt native SQLite-Bindungen, die in Cloudflare Workers, Vercel Edge Functions und ähnlichen Umgebungen fehlschlagen. Verwenden Sie stattdessen@libsql/client/web. - String-Interpolation in SQL –
execute(\SELECT * FROM users WHERE id = '${id}'`)ist eine SQL-Injection-Sicherheitslücke. Verwenden Sie immer parametrisierte Abfragen mitargs`. - Direkter Zugriff auf die eingebettete Replikatdatei – Das Öffnen der lokalen
.db-Datei mit einem anderen SQLite-Client, während der libSQL-Client läuft, führt zu Datenkorruption. Greifen Sie nur über den Client darauf zu.
Probleme mit mittlerer Priorität:
- Verwendung von
transaction(), wennbatch()ausreicht – Interaktive Transaktionen halten einen Datenbank-Lock mit einem 5-Sekunden-Timeout, erfordern mehrere Roundtrips und blockieren andere Schreiber. Verwenden Siebatch()für vorbestimmte Anweisungssätze. - Nicht-Aufruf von
client.close()– In kurzlebigen Prozessen (CLI-Skripte, Test-Tear-Down) führt das Vergessen, den Client zu schließen, zu offenen Verbindungen und Dateihandles. - Ignorieren der Schreiblatenz bei eingebetteten Replikaten – Lesevorgänge sind Mikrosekunden (lokal), aber Schreibvorgänge dauern 15–50 ms (an den entfernten Primärserver weitergeleitet). Planen Sie entsprechend – vermeiden Sie enge Schreibschleifen.
- Zu niedriges
syncInterval– Jede Synchronisation zieht alle geänderten Frames (je 4KB). Sub-Sekunden-Intervalle bei schreibintensiven Datenbanken verursachen erheblichen Netzwerk- und I/O-Overhead.
Häufige Fehler:
- Falscher Paketname – Das Paket heißt
@libsql/client, nichtlibsql-client,@turso/clientoderturso-client. - Benannter Parameter-Präfix im Args-Objekt – Args verwenden nackte Namen:
{ name: "Alice" }entspricht:name,@nameund$namein SQL. Fügen Sie den Präfix nicht hinzu:{ ":name": "Alice" }wird nicht übereinstimmen. - Erwartung, dass
lastInsertRowideine Zahl ist – Es istbigint | undefined. Wenn Sie eine Zahl benötigen, konvertieren Sie explizit:Number(result.lastInsertRowid), aber seien Sie sich des Präzisionsverlusts bei sehr großen Rowids bewusst. - Verwendung von
executeMultiple()für atomare Operationen –executeMultiple()führt rohen SQL-Text (durch Semikolon getrennt) ohne Parametrisierung und ohne implizite Transaktion aus. Verwenden Siebatch()für atomare parametrisierte Operationen.
Besonderheiten & Grenzfälle:
batch()-Anweisungen teilen eine Transaktion, sind aber NICHT parallel – Sie werden sequenziell ausgeführt.last_insert_rowid()funktioniert nur, weil die Anweisungen in derselben Transaktion ausgeführt werden.
</red_flags>