Cloudflare D1 répond "params with multiple statements is not supported". Les paramètres liés et le SQL multi-instructions s’excluent, donc vous ne pouvez pas envelopper plusieurs écritures paramétrées dans un BEGIN et un COMMIT. Je l’ai vérifié sur la base réelle plutôt que déduit, et cela change l’ordre de chaque chemin d’écriture.
En quoi consiste la restriction
Vous pouvez envoyer plusieurs instructions dans une chaîne. Vous pouvez envoyer une instruction avec des paramètres liés. Pas les deux. Dès qu’un envoi contient à la fois un point-virgule séparant des instructions et une liste de paramètres, D1 le rejette avec ce message exact.
Le contournement évident est pire que la restriction. Interpoler les valeurs dans le SQL pour obtenir le multi-instructions échange une transaction contre une surface d’injection, sur une base atteinte en HTTP, et aucune transaction ne vaut cela.
Ce que cela coûte
Un helper batch au-dessus de l’API REST est donc séquentiel et non atomique. Le mien émet les instructions l’une après l’autre. Si la connexion tombe en cours de route, la moitié des écritures a été appliquée et l’autre non, et rien ne revient en arrière.
Tout appelant doit être ordonné pour qu’un échec à mi-chemin laisse un état récupérable.
Cette phrase est toute la règle de conception. Ce n’est pas une limite à contourner, c’est une contrainte contre laquelle écrire, et le code n’en devient pas plus difficile. Il devient ordonné volontairement plutôt que par hasard.
Ordonner une écriture pour survivre à l’échec
Le calendrier de disponibilité est le cas le plus net que j’aie. Remplacer une semaine de créneaux, c’est une suppression et une insertion. Supprimez d’abord et une interruption laisse un calendrier vide, ce qui coupe silencieusement toutes les réservations et ressemble à une panne. Insérez d’abord et une interruption laisse des doublons, que le chemin de lecture regroupe déjà par jour et par heure.
// Not atomic. So the order is the recovery strategy: insert the new rows
// first, delete the old ids afterwards. An interruption between the two
// leaves duplicates, which the read path collapses, rather than an empty
// calendar, which nobody can recover from.
await db.batch(rows.map((r) => ({
sql: 'INSERT INTO availability (id, weekday, start_minute, end_minute) VALUES (?, ?, ?, ?)',
params: [r.id, r.weekday, r.start, r.end],
})));
await db.batch(oldIds.map((id) => ({
sql: 'DELETE FROM availability WHERE id = ?',
params: [id],
})));Les deux mêmes opérations, la même absence de transaction, et la différence entre un mauvais après-midi et personne qui s’en aperçoit tient à laquelle passe en premier.
Le piège des horodatages, de la même famille
Tant que vous écrivez des valeurs par défaut en SQL, utilisez la forme ISO avec le T : strftime('%Y-%m-%dT%H:%M:%fZ', 'now'). La variante avec espace paraît équivalente et se trie autrement, donc les comparaisons de chaînes contre un toISOString JavaScript cessent de correspondre, en silence, sur les lignes écrites par la base et non par votre code.
La règle que je donnerais
- Partez du principe qu’il n’y a pas de transactions. Ordonnez chaque chemin multi-écritures pour qu’une lecture survive à l’état intermédiaire.
- Préférez les doublons à l’absence. Une lecture qui déduplique coûte peu ; un utilisateur dont les données ont disparu, non.
- N’interpolez jamais de valeurs pour obtenir le multi-instructions.
- Écrivez les valeurs par défaut du schéma sous la forme ISO avec T, et rien d’autre.