Zum Inhalt springen

Queries & Live-Daten

Erstelle einen Client mit deinem Schema, der Worker-URL, einer Workspace-ID und einem Token-Callback. Deine Anwendung liefert diese Verbindungswerte.

import { createClient } from "tapedb";
import schema from "./tapedb.schema";
const db = createClient({ schema, url, workspace, getToken });
const open = await db.task.findMany({
where: { done: false, due: { lt: Date.now() } },
orderBy: { due: "asc" },
take: 50,
include: { column: { include: { board: true } } },
});

findMany gibt jede passende Zeile zurück, sofern du kein take übergibst; take und findPage haben keine Obergrenze. Breite Reads laufen unter der Haube als begrenzte Statements: Keyset-Chunks von 1.000 Zeilen, wobei Includes 1.000 Eltern gleichzeitig hydratisieren.

const one = await db.task.findUnique({ where: { id } }); // Zeile oder null
const total = await db.task.count({ where: { done: false } });
const page = await db.task.findPage({ orderBy: { due: "asc" }, take: 50 });
// { rows, hasMore, cursor } — Cursor als `after` zurückgeben
const first = await db.task.findFirst({ where: { done: false } }); // erster Treffer oder null

Paginierung ist Keyset, daher kann ein Insert oberhalb des Fensters keine Zeile wiederholen.

select gibt nur die benannten Felder zurück; Relationen in select bekommen ihr eigenes select:

const titles = await db.task.findMany({
select: { title: true, column: { select: { name: true } } },
});
// { title: string; column: { name: string } | null }[]

Die Engine liest und sendet nur diese Spalten, plus die ID der Zeile, auf die Live-Updates schlüsseln. Ein Live-Select bekommt nur dann ein Update, wenn sich ein ausgewähltes Feld geändert hat. select und include werden nicht auf derselben Ebene kombiniert.

Aggregates nehmen dieselbe where und geben nur die gefragten Maße zurück:

const stats = await db.task.aggregate({
where: { done: false },
_count: true, // oder { _all: true, due: true } für Non-Null-Counts
_sum: { points: true },
_avg: { points: true },
_min: { due: true },
_max: { due: true },
});
const perColumn = await db.task.groupBy({
by: ["columnId"],
_count: true,
_sum: { points: true },
orderBy: { columnId: "asc" },
take: 20,
});

Sie laufen als ein GROUP BY-Statement über die Zeilen, die der Aufrufer lesen darf. Eine filter()-Leseregel wird hineinkompiliert; ein Modell mit einer Funktion als Leseregel wird Zeile für Zeile angewendet, nachdem die Regel lief. Counts und Aggregates sind live mit subscribe.

Filter unterstützen not, in, notIn, gt, gte, lt, lte sowie AND / OR / NOT über ganze Filter.

Eine to-one-Relation nimmt einen Filter auf die Zeile, auf die sie zeigt; eine to-many-Relation nimmt some, every oder none:

await db.column.findMany({ where: { board: { archived: false } } });
await db.board.findMany({ where: { columns: { some: { tasks: { none: {} } } } } });
await db.task.findMany({ where: { column: null } }); // kein Parent

Diese kompilieren zu korrelierten Subqueries, verknüpfte Zeilen werden also nie abgerufen. Die Leseregel des verknüpften Modells wird in die Subquery kompiliert; ein Modell mit einer Funktion als Leseregel wird als Filterziel abgelehnt. Typen folgen einem Link; weitere Hops prüft die Engine. Relationsfilter verschachteln bis zur Ausdruckstiefe von SQLite, etwa acht Hops; breite Filter sind nicht begrenzt.

Alles, was die Engine entscheidet (Rechte, eine fehlende Zeile, ein Konflikt), kommt beim Await der Query oder im error einer Live-Query hoch.

Eine Query wird live, wenn du sie abonnierst:

const stop = db.task
.findMany({ where: { done: false }, orderBy: { due: "asc" } })
.subscribe(({ data }) => renderTasks(data));
// Wenn die Ansicht verworfen wird:
stop();

Nach einem Commit laufen nur Subscriptions erneut, deren Query ein geändertes Modell berührt, gebündelt alle 50 ms, und Clients erhalten Diffs pro Zeile. Counts und Aggregates bleiben ebenfalls live. Ein Live-select bekommt nur dann ein Update, wenn sich ein ausgewähltes Feld geändert hat.

React-Bindings (useLive, useLivePage, usePresence, useBroadcast) stehen in React.