Un savepoint es un marcador con nombre dentro de una transacción
Una transacción normal es todo o nada: lo que está entre BEGIN y COMMIT se guarda en bloque o se descarta en bloque. Casi siempre eso es justo lo que quieres, pero hay veces en que conviene tener más granularidad. Algo así como: "intenta este lote de cambios; si algo sale mal, deshaz solo ese lote y deja viva el resto de la transacción".
Para eso sirve un savepoint en SQLite. Pones un marcador con nombre, haces tu trabajo y luego decides: o lo confirmas (RELEASE) o vuelves atrás hasta el marcador (ROLLBACK TO).
Tanto el débito de Ada como el crédito de Boris quedan aplicados. La actualización errónea a Nobody se revirtió sin perder el resto de la transacción.
Los tres comandos
Toda la API se reduce a tres sentencias:
SAVEPOINT nombre— coloca un marcador.RELEASE SAVEPOINT nombre— conserva el trabajo hecho desde el marcador y lo elimina.ROLLBACK TO SAVEPOINT nombre— deshace todo lo ocurrido desde el marcador, pero el marcador se mantiene para que puedas volver a intentarlo.
La palabra SAVEPOINT después de RELEASE y ROLLBACK TO es opcional: tanto RELEASE risky como ROLLBACK TO risky funcionan igual.
La fila intento del paso 2 nunca existió desde el punto de vista de la base de datos final. Todo lo demás sí queda guardado.
Savepoints sin una transacción externa
Aquí va un pequeño detalle: puedes ejecutar SAVEPOINT sin un BEGIN previo. SQLite abre una transacción por ti de forma silenciosa, y el savepoint más externo hace las veces de la propia transacción. Un RELEASE sobre ese savepoint hace el commit, mientras que ROLLBACK TO deshace los cambios sin confirmarlos.
Por eso a los savepoints se les llama a veces "transacciones con nombre". Pero mezclar los dos estilos en código real se vuelve un lío: elige uno y quédate con él. Lo habitual es usar BEGIN ... COMMIT explícito para delimitar la transacción externa y reservar los savepoints solo para los puntos internos donde quieras poder deshacer cambios parciales.
Savepoints anidados en SQLite
Los savepoints se apilan. Puedes crear uno dentro de otro y hacer rollback del interno sin que eso afecte al externo:
Resultado final: a, b, d. Al hacer ROLLBACK TO SAVEPOINT inner se eliminó c, pero el trabajo previo a inner (la inserción de b) se mantuvo intacto y la transacción siguió su curso.
Hacer rollback a un savepoint externo también descarta todo lo hecho en niveles internos: toda la pila por encima de ese nombre se deshace de golpe:
Tanto b como c desaparecen. ROLLBACK TO outer revierte todo lo ocurrido desde que se creó outer, incluyendo inner y el insert de c.
¿Para qué sirven los savepoints?
El caso típico es procesar un lote en el que algunos elementos pueden fallar sin que eso eche a perder todo el lote. Envuelves cada elemento en un savepoint; si falla, haces rollback hasta ese savepoint y sigues con el siguiente:
En código real de aplicación, el INSERT defectuoso lanza un error, la aplicación lo captura, ejecuta ROLLBACK TO y sigue adelante. Las dos filas buenas quedan guardadas y la fila problemática no contamina el lote.
Este patrón es justamente el que usan los ORMs y las herramientas de migración para implementar transacciones anidadas en SQLite: en realidad no anidan bloques BEGIN (SQLite no lo permite), sino que traducen las llamadas anidadas a savepoints.
Detalles a tener en cuenta
Hay un puñado de matices que suelen pillar desprevenidos a quienes empiezan con savepoints:
COMMITsiempre confirma toda la transacción. Da igual cuántos savepoints tengas abiertos:COMMIT(o su aliasEND) cierra la transacción externa completa. No pienses enRELEASEcomo un commit parcial; nada se vuelve durable hasta que la transacción que lo envuelve haga commit.ROLLBACK(sinTO) cancela todo. Termina la transacción y descarta todos los savepoints abiertos. UsaROLLBACK TO nombrecuando quieras mantener viva la transacción.- Un savepoint sigue abierto hasta que se libera o se hace rollback hasta él. Olvidarte de hacer
RELEASEno implica perder datos, simplemente el marcador queda ahí hasta que termine la transacción. - Los nombres no tienen por qué ser únicos. Si declaras
SAVEPOINT sdos veces,ROLLBACK TO sencuentra el más reciente. Útil para recursión, confuso si pasa por accidente.
Lo que viene: vistas
Los savepoints te dan un control más fino sobre las escrituras. El siguiente paso es moldear cómo lees: guardar una consulta como un objeto con nombre y reutilizable, sobre el que puedes hacer SELECT igual que en una tabla. Eso es una vista, y la veremos a continuación.
Preguntas frecuentes
¿Qué es un savepoint en SQLite?
Un savepoint es un marcador con nombre que colocas dentro de una transacción. Más adelante puedes hacer ROLLBACK TO con ese nombre para deshacer todo lo que pasó después, o RELEASE para conservar los cambios y descartar el marcador. En la práctica te permiten tratar partes de una transacción como bloques más pequeños y recuperables.
¿En qué se diferencia un savepoint de una transacción en SQLite?
Una transacción empieza con BEGIN y termina con COMMIT o ROLLBACK. Un savepoint, en cambio, se crea dentro de una transacción con SAVEPOINT nombre y te da un punto de deshacer parcial. Volver a un savepoint no cierra la transacción que lo contiene: puedes seguir trabajando y hacer commit más adelante.
¿Se pueden anidar savepoints en SQLite?
Sí. Puedes apilar savepoints con distintos nombres, y un ROLLBACK TO outer deshace todo hasta ese nivel, incluidos los savepoints internos. Los nombres tampoco tienen que ser únicos: SQLite usa el savepoint más reciente con ese nombre, que es el comportamiento clásico de una pila.