Cloudflare D1 responde "params with multiple statements is not supported". Los parámetros vinculados y el SQL de varias sentencias se excluyen mutuamente, así que no puedes envolver varias escrituras parametrizadas en un BEGIN y un COMMIT. Lo comprobé contra la base de datos real en vez de deducirlo, y cambia cómo hay que ordenar cada ruta de escritura.
En qué consiste la restricción
Puedes mandar varias sentencias en una cadena. Puedes mandar una sentencia con parámetros vinculados. No puedes hacer las dos cosas. En cuanto un envío lleva a la vez un punto y coma separando sentencias y una lista de parámetros, D1 lo rechaza con ese mensaje exacto.
El apaño evidente es peor que la restricción. Interpolar valores en el SQL para conseguir varias sentencias cambia una transacción por una superficie de inyección, en una base de datos a la que se llega por HTTP, y ninguna transacción vale eso.
Lo que cuesta
Significa que un helper de batch sobre la API REST es secuencial y no atómico. El mío lanza las sentencias una detrás de otra. Si la conexión se cae a mitad, la mitad de las escrituras entraron y la otra mitad no, y nada hace rollback.
Quien llame tiene que estar ordenado para que un fallo a medio camino deje un estado recuperable.
Esa frase es toda la regla de diseño. No es una limitación que esquivar, es una restricción contra la que escribir código, y el código no sale más difícil. Sale ordenado a propósito en vez de por casualidad.
Ordenar una escritura para que el fallo se sobreviva
El calendario de disponibilidad es el caso más claro que tengo. Reemplazar una semana de huecos es un borrado y un insert. Si borras primero, una interrupción deja el calendario vacío, lo que corta todas las reservas en silencio y parece que la función está rota. Si insertas primero, una interrupción deja duplicados, que la ruta de lectura ya colapsa porque agrupa por día y hora.
// 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],
})));Las mismas dos operaciones, la misma ausencia de transacción, y la diferencia entre una mala tarde y que nadie se entere es cuál va primero.
La trampa de las marcas de tiempo, de la misma familia
Ya que estás escribiendo defaults en SQL, usa la forma ISO con la T: strftime('%Y-%m-%dT%H:%M:%fZ', 'now'). La variante con espacio parece equivalente y ordena distinto, así que las comparaciones de cadenas contra un toISOString de JavaScript dejan de casar, en silencio, en las filas que escribe la base de datos y no tu código.
La regla que yo te daría
- Da por hecho que no hay transacciones. Ordena cada ruta de varias escrituras para que el estado intermedio lo aguante una lectura.
- Prefiere duplicados a ausencias. Una lectura que deduplica es barata; un usuario cuyos datos desaparecieron no lo es.
- No interpoles valores nunca para conseguir varias sentencias.
- Escribe los defaults del esquema en la forma ISO con T y en ninguna otra.