Base de données
Chaque application dispose de sa propre base SQLite, accessible par sdk.db. La base est
durable et limitée à votre application : une autre application ne peut ni la lire ni y écrire
(voir le client createSdk dans la Présentation du SDK). Il n'y a rien à
provisionner : la base est créée au premier appel.
API
import { createSdk } from "@bext-stack/framework/sdk";
const sdk = createSdk("<app-id>");
const posts = await sdk.db.all<{ id: number; title: string }>(
"SELECT id, title FROM posts WHERE status = ? ORDER BY id DESC",
["published"],
);
const post = await sdk.db.get("SELECT * FROM posts WHERE id = ?", [42]);
const { changes, lastInsertId } = await sdk.db.exec(
"INSERT INTO posts (title, status) VALUES (?, ?)",
["Bonjour", "draft"],
);
| Méthode | Description |
|---|---|
all<T>(sql, params?) |
Exécuter une requête et retourner toutes les lignes, sous forme d'objets { colonne: valeur }. |
get<T>(sql, params?) |
Comme all, mais ne retourne que la première ligne, ou null si aucune ne correspond. |
exec(sql, params?) |
Exécuter une écriture (CREATE, INSERT, UPDATE, DELETE…). Retourne { changes, lastInsertId }. |
changes est le nombre de lignes modifiées, lastInsertId le rowid de la dernière insertion.
Appelez sdk.db côté serveur : loader, action, route.ts ou tâche (dans une tâche, le même
client est disponible sous ctx.db, voir Tâches et planificateur).
En cas d'échec, chaque méthode lève une Error qui porte le message de SQLite (table inconnue,
contrainte violée, erreur de syntaxe…). Une requête qui ne trouve rien n'est pas une erreur :
all retourne [] et get retourne null.
Les valeurs lues arrivent avec leur type SQLite : un entier ou un réel devient un number, un
texte une string, NULL devient null. Un BLOB est retourné encodé en base64.
Paramètres
Écrivez des ? dans le texte SQL et passez les valeurs dans le tableau params, dans l'ordre.
Une valeur passée ainsi est liée comme une donnée : elle n'est jamais concaténée au SQL. Ne
construisez pas la requête par concaténation de chaînes avec des valeurs venues de l'utilisateur.
Le client envoie chaque paramètre sous forme de chaîne :
- un nombre devient sa représentation texte (
42devient"42") ; nulletundefineddeviennent une chaîne vide, pasNULL;- un booléen devient
"true"ou"false", un objet"[object Object]".
En pratique :
- Déclarez le type des colonnes (
INTEGER,REAL,TEXT). Une colonneINTEGERconvertit"42"en entier à l'écriture comme à la comparaison, doncWHERE id = ?avec[42]fonctionne. Une colonne sans type garde la chaîne telle quelle, etWHERE n = 42ne la retrouve plus. - Pour une valeur nulle, écrivez
NULLdans le SQL plutôt que de passernull:UPDATE posts SET published_at = NULL WHERE id = ?. - Stockez les booléens en
1/0, et les objets avecJSON.stringifydans une colonneTEXT.
Une instruction par appel
Chaque appel exécute une seule instruction SQL. Si le texte en contient plusieurs, séparées par
;, seule la première est exécutée. Créez vos tables et index avec un appel exec chacun.
Il n'y a pas de transaction qui couvre plusieurs appels : chaque appel est indépendant, et un
BEGIN laissé ouvert est annulé à la fin de l'appel. Pour écrire plusieurs lignes de façon
atomique, faites-le en une seule instruction :
await sdk.db.exec(
"INSERT INTO tags (post_id, name) VALUES (?, ?), (?, ?), (?, ?)",
[postId, "prism", postId, "sqlite", postId, "sdk"],
);
Une écriture qui échoue en cours de route (contrainte, délai dépassé) ne laisse aucune ligne partiellement écrite.
Écritures avec RETURNING
exec refuse une instruction qui retourne des lignes : un INSERT … RETURNING y échoue avant
d'écrire quoi que ce soit. Passez-la à all (ou get), qui l'exécute dans une transaction et
retourne les lignes :
const created = await sdk.db.get<{ id: number }>(
"INSERT INTO posts (title, status) VALUES (?, ?) RETURNING id",
["Bonjour", "draft"],
);
Limites
| Limite | Valeur |
|---|---|
| Durée d'exécution d'un appel côté base | 5 secondes par défaut, puis l'appel échoue (database query deadline exceeded) |
| Lignes retournées par un appel | 10 000 au plus |
| Taille d'un résultat | 16 Mio au plus |
| Délai du client | timeoutMs de createSdk, 30 secondes par défaut |
Un résultat qui dépasse une limite fait échouer tout l'appel : il n'est pas tronqué. get lit
toutes les lignes avant de garder la première : ajoutez LIMIT 1 à une requête qui peut en
retourner beaucoup. Paginez les
lectures larges avec LIMIT ? OFFSET ? ou, mieux, avec une condition sur la clé
(WHERE id < ? ORDER BY id DESC LIMIT ?).
Mettre en place le schéma
sdk.db n'a pas d'outil de migration : le schéma se crée depuis votre code, avec des
instructions idempotentes. Le motif courant est une fonction ensureSchema mémorisée sous forme de
promesse : elle s'exécute une fois par isolat, et les requêtes simultanées partagent la même
initialisation. Chaque isolat a son propre état de module, donc les instructions doivent pouvoir
être rejouées sans effet :
// src/lib/db.ts
import { createSdk } from "@bext-stack/framework/sdk";
export const sdk = createSdk("<app-id>");
let schema: Promise<void> | null = null;
export function ensureSchema(): Promise<void> {
schema ??= (async () => {
await sdk.db.exec(
"CREATE TABLE IF NOT EXISTS reservations (id INTEGER PRIMARY KEY, email TEXT NOT NULL, day TEXT NOT NULL, people INTEGER NOT NULL, created_at TEXT NOT NULL)",
);
await sdk.db.exec(
"CREATE INDEX IF NOT EXISTS reservations_day ON reservations (day)",
);
// Added later: ALTER TABLE fails if the column already exists.
try {
await sdk.db.exec("ALTER TABLE reservations ADD COLUMN status TEXT DEFAULT 'pending'");
} catch {}
})();
return schema;
}
// src/app/api/reservations/route.ts
import { ensureSchema, sdk } from "../../../lib/db";
export async function POST(request: Request) {
await ensureSchema();
const { email, day, people } = await request.json();
const { lastInsertId } = await sdk.db.exec(
"INSERT INTO reservations (email, day, people, created_at) VALUES (?, ?, ?, ?)",
[email, day, people, new Date().toISOString()],
);
return Response.json({ id: lastInsertId });
}
Ajoutez les évolutions à la fin de ensureSchema, sans modifier les instructions déjà en place.
ALTER TABLE … ADD COLUMN lève une erreur quand la colonne existe déjà : entourez-le d'un
try/catch, comme ci-dessus, ou vérifiez d'abord les colonnes avec
SELECT name FROM pragma_table_info('reservations').
sdk.db et les entités
sdk.db sert l'état propre à votre application : réservations, brouillons, journaux,
compteurs. Les données métier des tenants (contacts, factures, articles…) restent dans la
Platform API, que les écrans générés par defineEntity utilisent (voir
Entités).
Le tagged template sql du framework, présenté sur la page Entités, peut aussi préparer une requête
pour sdk.db : sa méthode compile() retourne { text, params } avec des paramètres ?.
import { sql } from "@bext-stack/framework";
const q = sql`SELECT id, title FROM posts WHERE id IN (${ids}) ORDER BY id`.compile();
const rows = await sdk.db.all(q.text, q.params);
Les paramètres suivent alors les mêmes règles que plus haut : ils partent sous forme de chaînes.
Et ensuite
| Page | Ce qu'elle couvre |
|---|---|
| Entités | defineEntity et le tagged template sql |
| Store KV | Un petit état clé/valeur, sans schéma |
| Loaders & Actions | Où s'exécutent les lectures et écritures |
| Présentation du SDK | Le client createSdk et l'interface BextSdk |