PostgreSQL no crea una transacción anidada cuando se envía un segundo BEGIN. Muestra una advertencia y mantiene la transacción actual; el primer COMMIT correcto la finaliza. Usa puntos de guardado cuando una función auxiliar necesita una reversión parcial. PostgreSQL: BEGIN

Esta guía ayuda a un desarrollador que trabaja en Mac Terminal a diagnosticar una función auxiliar que confirma demasiado pronto una operación de edición de artículos. Los ejemplos usan filas de artículos sintéticas y muestran cómo mantener un único responsable de la transacción y, a la vez, permitir que una función auxiliar deshaga su propio lote. Siguen la documentación actual de PostgreSQL y node-postgres.

¿Qué ocurre cuando BEGIN y COMMIT aparecen dentro de otra transacción?

Los comandos de transacción SQL operan sobre la transacción actual de la conexión. PostgreSQL no cuenta las llamadas repetidas a BEGIN para esperar llamadas correspondientes a COMMIT. Un segundo BEGIN no modifica la transacción; un COMMIT simple que se ejecute correctamente confirma esa transacción actual. Si después se emite otro COMMIT simple, se produce una advertencia porque no hay ninguna transacción abierta. PostgreSQL: BEGIN, PostgreSQL: COMMIT

En una transacción correcta, la secuencia de límites es la siguiente:

Comando enviado en una conexión Límite resultante
Primer BEGIN Abre la transacción
Primera inserción de un artículo Pertenece a esa transacción
Segundo BEGIN Muestra una advertencia; no crea un límite interno
Segunda inserción de un artículo Pertenece a la misma transacción
Primer COMMIT Confirma ambas inserciones y finaliza la transacción
Otra instrucción enviada por separado Se ejecuta en su propia transacción con el comportamiento normal de confirmación automática
Un ROLLBACK posterior No puede deshacer las inserciones ya confirmadas

Esto explica un fallo habitual en una aplicación: el llamador abre una transacción y luego invoca una función auxiliar que envía su propio BEGIN y COMMIT. El COMMIT de la función auxiliar también finaliza la transacción del llamador. Una excepción posterior en el llamador no puede restaurar la operación original de todo o nada. Esta conclusión se desprende de los límites de transacción documentados por PostgreSQL. PostgreSQL: Transactions

Reproduce la confirmación prematura en Mac Terminal

Necesitas una base de datos PostgreSQL en ejecución y el comando psql instalado con una configuración de conexión funcional. Abre Terminal e inicia una sesión nueva con psql -X. La opción -X omite los archivos de inicio, lo que hace que esta demostración dependa menos de las personalizaciones locales. Ejecuta el siguiente script en esa sesión; activa explícitamente la confirmación automática. PostgreSQL: psql

La tabla es temporal y solo contiene títulos de artículos sintéticos. ON COMMIT PRESERVE ROWS mantiene sus filas disponibles para inspeccionarlas entre límites de transacción; PostgreSQL elimina la tabla temporal cuando termina la sesión. Esto demuestra el comportamiento de reversión dentro de la sesión, en vez de mostrar almacenamiento persistente después de desconectarse. PostgreSQL: CREATE TABLE

\set AUTOCOMMIT on

CREATE TEMP TABLE article_boundary_demo (
    id integer PRIMARY KEY,
    title text NOT NULL
) ON COMMIT PRESERVE ROWS;

BEGIN;
INSERT INTO article_boundary_demo VALUES (1, 'Opening draft');

-- Deliberately incorrect attempt to open an inner transaction.
BEGIN;
INSERT INTO article_boundary_demo VALUES (2, 'Middle draft');
COMMIT;

-- The previous COMMIT has already ended the transaction.
INSERT INTO article_boundary_demo VALUES (3, 'Closing draft');
ROLLBACK;

SELECT id, title FROM article_boundary_demo ORDER BY id;

El resultado esperado contiene las tres filas. El segundo BEGIN advierte que ya hay una transacción en curso. El COMMIT confirma las filas 1 y 2; luego la fila 3 se confirma mediante la confirmación automática. El ROLLBACK final no tiene ninguna transacción abierta que deshacer. Estos son resultados esperados que se deducen del comportamiento documentado, no una ejecución de base de datos reportada. PostgreSQL: BEGIN, PostgreSQL: Transactions

Mantén esta reproducción en psql interactivo, tal como se muestra. La forma en que el cliente envía las instrucciones importa: varias instrucciones SQL pasadas juntas en una cadena mediante psql -c pueden compartir una transacción implícita, a menos que comandos de transacción explícitos las separen. La documentación recomienda usar comandos separados o la entrada estándar cuando se desea ejecutarlas por separado. PostgreSQL: psql

¿Cómo conservan los puntos de guardado la transacción externa?

Un punto de guardado marca una posición dentro de una transacción existente. ROLLBACK TO SAVEPOINT descarta los cambios posteriores a esa posición y conserva el trabajo anterior. RELEASE SAVEPOINT elimina la marca y mantiene los cambios que sobreviven dentro de la transacción contenedora. No confirma esos cambios de forma independiente. PostgreSQL: SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT

Ejecuta este ejemplo completo en la misma sesión o en otra sesión nueva de psql. Crea su propia tabla temporal:

CREATE TEMP TABLE article_savepoint_demo (
    id integer PRIMARY KEY,
    title text NOT NULL
) ON COMMIT PRESERVE ROWS;

BEGIN;
INSERT INTO article_savepoint_demo VALUES (1, 'Opening draft');

SAVEPOINT optional_article;
INSERT INTO article_savepoint_demo VALUES (2, 'Optional draft');
ROLLBACK TO SAVEPOINT optional_article;
RELEASE SAVEPOINT optional_article;

INSERT INTO article_savepoint_demo VALUES (3, 'Closing draft');
COMMIT;

SELECT id, title FROM article_savepoint_demo ORDER BY id;

Las filas esperadas son 1 y 3. La fila 2 se descarta en el punto de guardado, mientras que las filas inicial y final siguen formando parte de una misma transacción hasta su COMMIT final. Si reemplazaras ese COMMIT final por ROLLBACK, se descartarían ambas inserciones restantes. PostgreSQL: SAVEPOINT, PostgreSQL: Transactions

La duración de los puntos de guardado también importa al combinar funciones auxiliares. Revertir hasta un punto de guardado deja disponible ese punto con nombre, pero destruye los puntos de guardado posteriores. Liberar un punto de guardado lo elimina junto con todos los puntos de guardado activos creados después. El ejemplo libera explícitamente su marca después de la recuperación. PostgreSQL: ROLLBACK TO SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT

¿Cómo deben compartir la transacción las funciones auxiliares de node-postgres?

node-postgres requiere que todas las instrucciones de una transacción usen la misma instancia de cliente y no proporciona una abstracción automática para anidar transacciones. Pasa a las funciones auxiliares el cliente del llamador. Si la aplicación usa un pool, adquiere un cliente con pool.connect(), úsalo durante toda la operación y libéralo después; pool.query() puede enviar consultas sucesivas a distintos clientes. node-postgres: Transactions, node-postgres: Pool

El siguiente programa completo usa un único Client directo. Su llamador es responsable de BEGIN, COMMIT y del ROLLBACK completo. La función auxiliar crea un punto de guardado alrededor de un lote opcional de dos filas. Su segunda inserción repite deliberadamente el ID de artículo 1, lo que provoca un error de unicidad; la recuperación descarta todo el lote opcional, incluida la inserción anterior que se había realizado correctamente.

Configura y ejecuta el ejemplo completo

Usa una versión actual compatible de Node.js con npm instalado. En una carpeta de trabajo, ejecuta npm install pg y luego guarda el programa siguiente como article-transactions.mjs. Usa tu entorno de conexión existente de PostgreSQL, incluidas las variables PGHOST, PGPORT, PGDATABASE y PGUSER cuando estén configuradas. Esos ajustes deben identificar una base de datos funcional antes de ejecutar el ejemplo. node-postgres: Welcome, node-postgres: Connecting

import pg from 'pg'
const { Client } = pg

async function addOptionalArticles(client) {
  await client.query('SAVEPOINT optional_articles')
  try {
    await client.query(
      'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
      [2, 'Optional draft']
    )
    await client.query(
      'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
      [1, 'Duplicate draft']
    )
  } catch (error) {
    if (error.code !== '23505') throw error
    await client.query('ROLLBACK TO SAVEPOINT optional_articles')
    await client.query('RELEASE SAVEPOINT optional_articles')
    return false
  }
  await client.query('RELEASE SAVEPOINT optional_articles')
  return true
}

async function main() {
  const client = new Client({ connectionTimeoutMillis: 5000 })
  await client.connect()
  try {
    await client.query(`
      CREATE TEMP TABLE article_demo (
        id integer PRIMARY KEY,
        title text NOT NULL
      ) ON COMMIT PRESERVE ROWS
    `)
    await client.query('BEGIN ISOLATION LEVEL READ COMMITTED')
    try {
      await client.query(
        'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
        [1, 'Opening draft']
      )
      const optionalKept = await addOptionalArticles(client)
      console.log('Optional batch kept:', optionalKept)
      await client.query(
        'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
        [3, 'Closing draft']
      )
      await client.query('COMMIT')
    } catch (error) {
      try {
        await client.query('ROLLBACK')
      } catch (rollbackError) {
        throw new AggregateError(
          [error, rollbackError], 'Transaction and rollback failed'
        )
      }
      throw error
    }
    const result = await client.query(
      'SELECT id, title FROM article_demo ORDER BY id'
    )
    console.table(result.rows)
  } finally {
    await client.end()
  }
}

main().catch(error => {
  console.error(error)
  process.exitCode = 1
})

Ejecuta node article-transactions.mjs. El resultado esperado indica Optional batch kept: false y muestra los ID de artículos 1 y 3. Los valores usan la interfaz de parámetros documentada query(text, values). Se comprobó la sintaxis de JavaScript con Node.js; el escenario de base de datos se contrastó con la documentación, pero no se ejecutó aquí. node-postgres: Queries

Recupera únicamente de un fallo aceptado de forma explícita

PostgreSQL asigna 23505 a unique_violation y recomienda comprobar los códigos de error en lugar del texto del mensaje. Aquí, aceptar ese código es una regla ilustrativa para el lote sintético opcional. Los demás errores se propagan al llamador, que intenta revertir toda la transacción. La función auxiliar devuelve un resultado explícito para que el llamador pueda ver que se descartó el trabajo opcional. PostgreSQL: PostgreSQL Error Codes

Capturar una excepción de JavaScript por sí solo no restaura una transacción fallida de PostgreSQL. Para recuperarse, hay que revertir hasta un punto de guardado establecido previamente o revertir la transacción completa. En particular, una transacción abortada no puede recuperarse simplemente liberando su punto de guardado. PostgreSQL: Transactions, PostgreSQL: RELEASE SAVEPOINT

La función auxiliar presupone una transacción activa y un uso secuencial de este cliente. No es un envoltorio general para transacciones recursivas: una abstracción de anidamiento reutilizable también requeriría gestionar explícitamente la propiedad y los nombres de los puntos de guardado.

¿Qué límites de aislamiento quedan después de corregir los límites de transacción?

Los puntos de guardado permiten una reversión parcial dentro de la transacción actual; no establecen una política de aislamiento aparte. Elige el nivel de aislamiento en el límite externo. PostgreSQL no permite cambiarlo después de la primera consulta o instrucción de modificación de datos de la transacción, así que una función auxiliar no puede elevar el nivel de una transacción ya utilizada enviando otro BEGIN. PostgreSQL: SAVEPOINT, PostgreSQL: SET TRANSACTION

Para las consultas habituales, conserva estas distinciones:

Nivel de aislamiento Límite pertinente
READ COMMITTED Las consultas sucesivas pueden ver los cambios recién confirmados por otras transacciones. Una transacción no implica que se use una instantánea fija.
REPEATABLE READ Las consultas comparten la instantánea establecida por la primera instrucción que no sea de control de transacciones, y a la vez ven sus propios cambios anteriores. Aún pueden producirse fallos de serialización.
SERIALIZABLE Las transacciones confirmadas correctamente tienen un efecto coherente con algún orden serial. Las aplicaciones aún deben gestionar los fallos de serialización.
READ UNCOMMITTED PostgreSQL lo trata como READ COMMITTED.

PostgreSQL: Transaction Isolation, PostgreSQL: SET TRANSACTION

Un fallo de serialización requiere volver a ejecutar la transacción completa, incluidas las decisiones que produjeron su SQL. Reintentar solo el trabajo del punto de guardado de la función auxiliar no actualiza la instantánea de la transacción externa. PostgreSQL identifica los fallos de serialización con el SQLSTATE 40001. El programa de demostración propaga estos fallos; no implementa reintentos automáticos. PostgreSQL: Transaction Isolation

¿Cómo se elige el límite adecuado para una función auxiliar?

Al revisar un llamador y sus funciones auxiliares para artículos, sigue esta secuencia de decisiones:

  1. Todo debe completarse correctamente en conjunto: deja que el llamador sea responsable de la transacción y pasa su cliente a las funciones auxiliares, que solo ejecutan consultas.
  2. Se puede descartar un lote mientras el llamador continúa: establece un punto de guardado antes de ese lote, recupérate solo de los fallos aceptados y devuelve el resultado explícitamente.
  3. El lote debe confirmarse de forma independiente: usa otro cliente y otra transacción. Su confirmación sobrevivirá a una reversión externa, por lo que no podrá satisfacer la tarea original de todo o nada.
  4. Una ejecución concurrente produce un fallo de serialización: reinicia la transacción completa en lugar de tratar la función auxiliar como un límite de reintento independiente.

Estas opciones se ajustan a la semántica de los puntos de guardado de PostgreSQL y al requisito de node-postgres de mantener una transacción en un único cliente. PostgreSQL: SAVEPOINT, node-postgres: Transactions, PostgreSQL: Transaction Isolation

Para la tarea de edición de artículos que se muestra aquí, mantén un único responsable de la transacción, pasa un solo cliente por toda la cadena de llamadas y reserva un punto de guardado para el trabajo opcional. Revisa los comandos de transacción de cada función auxiliar: un COMMIT inesperado cambia el límite de toda la conexión, mientras que liberar un punto de guardado sigue dejando el resultado final en manos del llamador.

Fuentes