Code checks for App Devs, using new options for transaction behavior
In the past, developers have wondered how we can provide "--dry-run"
functionality
/messages/by-id/15791.1450383201@sss.pgh.pa.us
This is important for application developers, especially when
migrating programs to Postgres.
Presented here are 3 features aimed at developers, each of which is
being actively used by me in a large and complex migration project.
* psql --parse-only
Checks the syntax of all SQL in a script, but without actually
executing it. This is very important in the early stages of complex
migrations because we need to see if the code would generate syntax
errors before we attempt to execute it. When there are many
dependencies between objects, actual execution fails very quickly if
we run in a single transaction, yet running outside of a transaction
can leave a difficult cleanup task. Fixing errors iteratively is
difficult when there are long chains of dependencies between objects,
since there is no easy way to predict how long it will take to make
everything work unless you understand how many syntax errors exist in
the script.
001_psql_parse_only.v1.patch
* nested transactions = off (default) | all | on
Handle nested BEGIN/COMMIT, which can cause chaos on failure. This is
an important part of guaranteeing that everything that gets executed
is part of a single atomic transaction, which can then be rolled back
- this is a pre-requisite for the last feature.
002_nested_xacts.v7.patch
The default behavior is unchanged (off)
Setting "all" treats nested BEGIN/COMMIT as subtransactions, allowing
some parts to fail without rolling back the outer transaction.
Setting "outer" flattens nested BEGIN/COMMIT into one single outer
transaction, so that any failure rolls back the entire transaction.
* rollback_on_commit = off (default) | on
Force transactions to fail their final commit, ensuring that no
lasting change is made when a script is tested. i.e. accept COMMIT,
but do rollback instead.
003_rollback_on_commit.v1.patch
We will probably want to review these on separate threads, but the
common purpose of these features is hopefully clear from these notes.
001 and 003 are fairly small patches, 002 is longer.
Comments please
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
002_nested_xacts.v7.patchapplication/octet-stream; name=002_nested_xacts.v7.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 559eb898a9..bea1d4fc1a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8880,6 +8880,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will be abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 72af656060..50ccb0daca 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -894,3 +894,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fd5103a78e..7c049b49f5 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,12 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2055,8 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3000,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3012,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
}
void
@@ -3012,6 +3021,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
}
@@ -3182,7 +3192,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3215,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3765,13 +3778,37 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ /* else drop thru */
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("there is already a transaction in progress")));
+ if (XactNesting == XACT_NEST_OUTER)
+ XactNestingLevel++;
+
break;
/* These cases are invalid. */
@@ -3815,6 +3852,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3863,6 +3904,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3871,7 +3913,19 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else
+ s->blockState = TBLOCK_END;
result = true;
break;
@@ -3900,6 +3954,16 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3908,8 +3972,14 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3917,7 +3987,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3931,8 +4020,14 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3942,6 +4037,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4022,6 +4129,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4031,7 +4139,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4041,7 +4156,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4050,8 +4177,14 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4061,6 +4194,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 05ab087934..1ddf1d9792 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -4533,6 +4540,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..1cdc697fe1 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,15 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +156,7 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..cf153a3393 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,229 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..4df2d4d382 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,136 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
003_rollback_on_commit.v1.patchapplication/octet-stream; name=003_rollback_on_commit.v1.patchDownload
commit f5cd3751d0d747ab5d7c0689ac8c0818c07b8580
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date: Wed Oct 26 15:47:22 2022 +0100
Rollback on commit
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bea1d4fc1a..bce7b847d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8846,6 +8846,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c049b49f5..9790dec0df 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,9 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
int DefaultXactNesting;
int XactNesting = XACT_NEST_OFF;
int XactNestingLevel = 0;
@@ -2057,6 +2060,7 @@ StartTransaction(void)
XactIsoLevel = DefaultXactIsoLevel;
XactNesting = DefaultXactNesting;
XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -3013,6 +3017,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3022,6 +3027,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -3915,18 +3921,26 @@ EndTransactionBlock(bool chain)
case TBLOCK_INPROGRESS:
if (XactNesting == XACT_NEST_OUTER)
{
- if (XactNestingLevel <= 0)
- s->blockState = TBLOCK_END;
- else
+ if (XactNestingLevel > 0)
+ {
ereport(NOTICE,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
XactNestingLevel--;
- return true;
+ }
+ if (XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
}
else
+ {
s->blockState = TBLOCK_END;
- result = true;
+ result = true;
+ }
break;
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1ddf1d9792..86d7f5e134 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1527,6 +1527,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index 1cdc697fe1..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT int XactIsoLevel;
extern PGDLLIMPORT int DefaultXactNesting;
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -157,6 +159,7 @@ typedef struct SavedTransactionCharacteristics
bool save_XactReadOnly;
bool save_XactDeferrable;
int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index cf153a3393..a30c869fc8 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1370,6 +1370,17 @@ SELECT * FROM abc ORDER BY 1;
(0 rows)
RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 4df2d4d382..e6f71d1fad 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -740,6 +740,16 @@ SELECT * FROM abc ORDER BY 1;
RESET nested_transactions;
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
On Thu, 27 Oct 2022 at 12:09, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Comments please
Update from patch tester results.
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
002_nested_xacts.v7.patchapplication/octet-stream; name=002_nested_xacts.v7.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 559eb898a9..bea1d4fc1a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8880,6 +8880,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will be abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 72af656060..50ccb0daca 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -894,3 +894,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fd5103a78e..7c049b49f5 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,12 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2055,8 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3000,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3012,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
}
void
@@ -3012,6 +3021,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
}
@@ -3182,7 +3192,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3215,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3765,13 +3778,37 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ /* else drop thru */
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("there is already a transaction in progress")));
+ if (XactNesting == XACT_NEST_OUTER)
+ XactNestingLevel++;
+
break;
/* These cases are invalid. */
@@ -3815,6 +3852,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3863,6 +3904,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3871,7 +3913,19 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else
+ s->blockState = TBLOCK_END;
result = true;
break;
@@ -3900,6 +3954,16 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3908,8 +3972,14 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3917,7 +3987,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3931,8 +4020,14 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3942,6 +4037,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4022,6 +4129,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4031,7 +4139,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4041,7 +4156,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4050,8 +4177,14 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4061,6 +4194,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 05ab087934..1ddf1d9792 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -4533,6 +4540,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..1cdc697fe1 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,15 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +156,7 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..cf153a3393 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,229 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..4df2d4d382 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,136 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
003_rollback_on_commit.v1.patchapplication/octet-stream; name=003_rollback_on_commit.v1.patchDownload
commit f5cd3751d0d747ab5d7c0689ac8c0818c07b8580
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date: Wed Oct 26 15:47:22 2022 +0100
Rollback on commit
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bea1d4fc1a..bce7b847d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8846,6 +8846,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c049b49f5..9790dec0df 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,9 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
int DefaultXactNesting;
int XactNesting = XACT_NEST_OFF;
int XactNestingLevel = 0;
@@ -2057,6 +2060,7 @@ StartTransaction(void)
XactIsoLevel = DefaultXactIsoLevel;
XactNesting = DefaultXactNesting;
XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -3013,6 +3017,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3022,6 +3027,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -3915,18 +3921,26 @@ EndTransactionBlock(bool chain)
case TBLOCK_INPROGRESS:
if (XactNesting == XACT_NEST_OUTER)
{
- if (XactNestingLevel <= 0)
- s->blockState = TBLOCK_END;
- else
+ if (XactNestingLevel > 0)
+ {
ereport(NOTICE,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
XactNestingLevel--;
- return true;
+ }
+ if (XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
}
else
+ {
s->blockState = TBLOCK_END;
- result = true;
+ result = true;
+ }
break;
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1ddf1d9792..86d7f5e134 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1527,6 +1527,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index 1cdc697fe1..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT int XactIsoLevel;
extern PGDLLIMPORT int DefaultXactNesting;
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -157,6 +159,7 @@ typedef struct SavedTransactionCharacteristics
bool save_XactReadOnly;
bool save_XactDeferrable;
int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index cf153a3393..a30c869fc8 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1370,6 +1370,17 @@ SELECT * FROM abc ORDER BY 1;
(0 rows)
RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 4df2d4d382..e6f71d1fad 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -740,6 +740,16 @@ SELECT * FROM abc ORDER BY 1;
RESET nested_transactions;
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
004_add_params_to_sample.v1.patchapplication/octet-stream; name=004_add_params_to_sample.v1.patchDownload
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 868d21c351..8e1e737d5d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,6 +687,8 @@
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
+#nested_transactions = 'off'
+#rollback_on_commit = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
Op 27-10-2022 om 18:35 schreef Simon Riggs:
On Thu, 27 Oct 2022 at 12:09, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Comments please
Update from patch tester results.
[001_psql_parse_only.v1.patch ]
[002_nested_xacts.v7.patch ]
[003_rollback_on_commit.v1.patch ]
[004_add_params_to_sample.v1.patch]
patch 002 has (2x) :
'transction' should be
'transaction'
also in patch 002:
'at any level will be abort' should be
'at any level will abort'
I also dislike the 'we' in
'Once we reach the top-level transaction,'
That seems a bit too much like the 'we developers working together to
make a database server system' which is of course used often and
usefully on this mailinglist and in code itself. But I think
user-facing docs should be careful with that team-building 'we'. I
remember well how it confused me, many years ago. Better, IMHO:
'Once the top-level transaction is reached,'
Thanks,
Erik Rijkers
On Fri, 28 Oct 2022 at 07:54, Erik Rijkers <er@xs4all.nl> wrote:
Op 27-10-2022 om 18:35 schreef Simon Riggs:
On Thu, 27 Oct 2022 at 12:09, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Comments please
Update from patch tester results.
[001_psql_parse_only.v1.patch ]
[002_nested_xacts.v7.patch ]
[003_rollback_on_commit.v1.patch ]
[004_add_params_to_sample.v1.patch]patch 002 has (2x) :
'transction' should be
'transaction'also in patch 002:
'at any level will be abort' should be
'at any level will abort'I also dislike the 'we' in
'Once we reach the top-level transaction,'
That seems a bit too much like the 'we developers working together to
make a database server system' which is of course used often and
usefully on this mailinglist and in code itself. But I think
user-facing docs should be careful with that team-building 'we'. I
remember well how it confused me, many years ago. Better, IMHO:'Once the top-level transaction is reached,'
Thanks for the feedback, I will make all of those corrections in the
next version.
I'm guessing you like the features??
--
Simon Riggs http://www.EnterpriseDB.com/
On Fri, 28 Oct 2022 at 10:33, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Thanks for the feedback, I will make all of those corrections in the
next version.
New version attached. I've rolled 002-004 into one patch, but can
split again as needed.
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
002_nested_xacts_and_rollback_on_commit.v8.patchapplication/octet-stream; name=002_nested_xacts_and_rollback_on_commit.v8.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 559eb898a9..0074acb910 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8846,6 +8846,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
@@ -8880,6 +8898,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once the top-level of nesting is reached,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 72af656060..50ccb0daca 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -894,3 +894,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fd5103a78e..43c2a93013 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,15 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2058,9 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3004,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3016,8 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3012,6 +3026,8 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -3182,7 +3198,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3221,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3765,13 +3784,40 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ else
+ ereport(WARNING,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("there is already a transaction in progress")));
+ break;
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("there is already a transaction in progress")));
+ if (XactNesting == XACT_NEST_OUTER)
+ XactNestingLevel++;
break;
/* These cases are invalid. */
@@ -3815,6 +3861,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3863,6 +3913,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3871,8 +3922,28 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
- result = true;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ XactNestingLevel--;
+ }
+ if (XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
+ }
+ else
+ {
+ s->blockState = TBLOCK_END;
+ result = true;
+ }
break;
/*
@@ -3900,6 +3971,16 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3908,8 +3989,14 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3917,7 +4004,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3931,8 +4037,14 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3942,6 +4054,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4022,6 +4146,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4031,7 +4156,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4041,7 +4173,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4050,8 +4194,14 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4061,6 +4211,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 05ab087934..86d7f5e134 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -1520,6 +1527,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
@@ -4533,6 +4549,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 868d21c351..8e1e737d5d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,6 +687,8 @@
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
+#nested_transactions = 'off'
+#rollback_on_commit = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,17 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +158,8 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..a30c869fc8 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,240 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..e6f71d1fad 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,146 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
On Sun, Oct 30, 2022 at 11:32 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:
On Fri, 28 Oct 2022 at 10:33, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Thanks for the feedback, I will make all of those corrections in the
next version.New version attached. I've rolled 002-004 into one patch, but can
split again as needed.
I like the idea of "parse only" and "nested xact", thanks for working
on this. I will look into patches in more detail, especially nested
xact. IMHO there is no point in merging "nested xact" and "rollback on
commit". They might be changing the same code location but these two
are completely different ideas, in fact all these three should be
reviewed as three separate threads as you mentioned in the first email
in the thread.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, Oct 31, 2022 at 4:23 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Sun, Oct 30, 2022 at 11:32 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:On Fri, 28 Oct 2022 at 10:33, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Thanks for the feedback, I will make all of those corrections in the
next version.New version attached. I've rolled 002-004 into one patch, but can
split again as needed.I like the idea of "parse only" and "nested xact", thanks for working
on this. I will look into patches in more detail, especially nested
xact. IMHO there is no point in merging "nested xact" and "rollback on
commit". They might be changing the same code location but these two
are completely different ideas, in fact all these three should be
reviewed as three separate threads as you mentioned in the first email
in the thread.
What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, Oct 31, 2022 at 5:03 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Oct 31, 2022 at 4:23 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Sun, Oct 30, 2022 at 11:32 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:On Fri, 28 Oct 2022 at 10:33, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Thanks for the feedback, I will make all of those corrections in the
next version.New version attached. I've rolled 002-004 into one patch, but can
split again as needed.I like the idea of "parse only" and "nested xact", thanks for working
on this. I will look into patches in more detail, especially nested
xact. IMHO there is no point in merging "nested xact" and "rollback on
commit". They might be changing the same code location but these two
are completely different ideas, in fact all these three should be
reviewed as three separate threads as you mentioned in the first email
in the thread.What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?
1.
@@ -3815,6 +3861,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
I am not sure whether it is good to not allow PREPARE or we can just
prepare it and come out of the complete nested transaction. Suppose
we have multiple savepoints and we say prepare then it will just
succeed so why does it have to be different here?
2. case TBLOCK_SUBABORT:
ereport(WARNING,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("there is already a transaction in progress")));
+ if (XactNesting == XACT_NEST_OUTER)
+ XactNestingLevel++;
break;
I did not understand what this change is for, can you tell me the
scenario or a test case to hit this?
Remaining part w.r.t "nested xact" patch looks fine, I haven't tested
it yet though.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, 31 Oct 2022 at 11:33, Dilip Kumar <dilipbalaut@gmail.com> wrote:
What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?
Patch does the same dance as with other xact variables.
XactNesting is the value within the transaction and in the patch this
is not exported, so cannot be set externally.
XactNesting is set at transaction start to the variable
DefaultXactNesting, which is set by the GUC.
So its not a problem, but thanks for checking.
--
Simon Riggs http://www.EnterpriseDB.com/
On Mon, 31 Oct 2022 at 12:22, Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Oct 31, 2022 at 5:03 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Oct 31, 2022 at 4:23 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Sun, Oct 30, 2022 at 11:32 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:On Fri, 28 Oct 2022 at 10:33, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
Thanks for the feedback, I will make all of those corrections in the
next version.New version attached. I've rolled 002-004 into one patch, but can
split again as needed.I like the idea of "parse only" and "nested xact", thanks for working
on this. I will look into patches in more detail, especially nested
xact. IMHO there is no point in merging "nested xact" and "rollback on
commit". They might be changing the same code location but these two
are completely different ideas, in fact all these three should be
reviewed as three separate threads as you mentioned in the first email
in the thread.What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?1.
@@ -3815,6 +3861,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);+ /* Don't allow prepare until we are back to an unnested state at level 0 */ + if (XactNestingLevel > 0) + return false;I am not sure whether it is good to not allow PREPARE or we can just
prepare it and come out of the complete nested transaction. Suppose
we have multiple savepoints and we say prepare then it will just
succeed so why does it have to be different here?
I'm happy to discuss what the behavior should be in this case. It is
not a common case,
and people don't put PREPARE in their scripts except maybe in a test.
My reasoning for this code is that we don't want to accept a COMMIT
until we reach top-level of nesting,
so the behavior should be similar for PREPARE, which is just
first-half of final commit.
Note that the nesting of begin/commit is completely separate to the
existence/non-existence of subtransactions, especially with
nested_transactions = 'outer'
2. case TBLOCK_SUBABORT: ereport(WARNING, (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), errmsg("there is already a transaction in progress"))); + if (XactNesting == XACT_NEST_OUTER) + XactNestingLevel++; break;I did not understand what this change is for, can you tell me the
scenario or a test case to hit this?
Well spotted, thanks. That seems to be some kind of artefact.
There is no test that exercises that since it is an unintended change,
so I have removed it.
Remaining part w.r.t "nested xact" patch looks fine, I haven't tested
it yet though.
New versions attached, separated again as you suggested.
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
002_nested_xacts.v9.patchapplication/octet-stream; name=002_nested_xacts.v9.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 559eb898a9..87e205d32d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8880,6 +8880,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once the top-level of nesting is reached,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 72af656060..50ccb0daca 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -894,3 +894,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fd5103a78e..9afcb2c37f 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,12 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2055,8 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3000,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3012,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
}
void
@@ -3012,6 +3021,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
}
@@ -3182,7 +3192,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3215,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3765,8 +3778,33 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ else
+ ereport(WARNING,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("there is already a transaction in progress")));
+ break;
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
@@ -3815,6 +3853,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3863,6 +3905,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3871,7 +3914,19 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else
+ s->blockState = TBLOCK_END;
result = true;
break;
@@ -3900,6 +3955,16 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3908,8 +3973,14 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3917,7 +3988,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3931,8 +4021,14 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3942,6 +4038,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4022,6 +4130,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4031,7 +4140,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4041,7 +4157,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4050,8 +4178,14 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4061,6 +4195,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 05ab087934..1ddf1d9792 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -4533,6 +4540,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 868d21c351..8e1e737d5d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,6 +687,8 @@
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
+#nested_transactions = 'off'
+#rollback_on_commit = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..1cdc697fe1 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,15 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +156,7 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..cf153a3393 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,229 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..4df2d4d382 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,136 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
003_rollback_on_commit.v1.patchapplication/octet-stream; name=003_rollback_on_commit.v1.patchDownload
commit f5cd3751d0d747ab5d7c0689ac8c0818c07b8580
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date: Wed Oct 26 15:47:22 2022 +0100
Rollback on commit
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bea1d4fc1a..bce7b847d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8846,6 +8846,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c049b49f5..9790dec0df 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,9 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
int DefaultXactNesting;
int XactNesting = XACT_NEST_OFF;
int XactNestingLevel = 0;
@@ -2057,6 +2060,7 @@ StartTransaction(void)
XactIsoLevel = DefaultXactIsoLevel;
XactNesting = DefaultXactNesting;
XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -3013,6 +3017,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3022,6 +3027,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -3915,18 +3921,26 @@ EndTransactionBlock(bool chain)
case TBLOCK_INPROGRESS:
if (XactNesting == XACT_NEST_OUTER)
{
- if (XactNestingLevel <= 0)
- s->blockState = TBLOCK_END;
- else
+ if (XactNestingLevel > 0)
+ {
ereport(NOTICE,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
XactNestingLevel--;
- return true;
+ }
+ if (XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
}
else
+ {
s->blockState = TBLOCK_END;
- result = true;
+ result = true;
+ }
break;
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1ddf1d9792..86d7f5e134 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1527,6 +1527,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index 1cdc697fe1..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT int XactIsoLevel;
extern PGDLLIMPORT int DefaultXactNesting;
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -157,6 +159,7 @@ typedef struct SavedTransactionCharacteristics
bool save_XactReadOnly;
bool save_XactDeferrable;
int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index cf153a3393..a30c869fc8 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1370,6 +1370,17 @@ SELECT * FROM abc ORDER BY 1;
(0 rows)
RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 4df2d4d382..e6f71d1fad 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -740,6 +740,16 @@ SELECT * FROM abc ORDER BY 1;
RESET nested_transactions;
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
On Mon, Oct 31, 2022 at 6:54 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:
What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?
I think you missed the above comment?
I am not sure whether it is good to not allow PREPARE or we can just
prepare it and come out of the complete nested transaction. Suppose
we have multiple savepoints and we say prepare then it will just
succeed so why does it have to be different here?I'm happy to discuss what the behavior should be in this case. It is
not a common case,
and people don't put PREPARE in their scripts except maybe in a test.My reasoning for this code is that we don't want to accept a COMMIT
until we reach top-level of nesting,
so the behavior should be similar for PREPARE, which is just
first-half of final commit.
Yeah this is not a very common case. And we can see opinions from
others as well. But I think your reasoning for doing it this way also
makes sense to me.
I have some more comments for 0002
1.
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts
outer transaction", XactNestingLevel--)));
+ }
I did not understand in case of 'outer' if we are giving rollback from
inner nesting level why it is throwing error? Documentation just says
this[1]+ <para> + A setting of <quote>outer</quote> will cause a nested + <command>BEGIN</command> to be remembered, so that an equal number + of <command>COMMIT</command> or <command>ROLLBACK</command> commands + are required to end the nesting. In that case a <command>ROLLBACK</command> + at any level will abort the entire outer transaction. + Once we reach the top-level transaction, + the final <command>COMMIT</command> will end the transaction. + This ensures that all commands within the outer transaction are atomic. + </para> but it did not
mention the error. I agree that we might need to give the rollback as
many times as the nesting level but giving errors seems confusing to
me.
[1]
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a
<command>ROLLBACK</command>
+ at any level will abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
2.
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u",
XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ while (s->parent != NULL && !found_subxact)
{
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT
I think these changes should be explained in the comments.
3.
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in
aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
+ }
Better to write this as if (XactNesting == XACT_NEST_OUTER &&
XactNestingLevel > 0) instead of two levels nested if conditions.
4.
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
I think this strcmp(s->name, NESTED_XACT_NAME) is done because there
could be other types of internal subtransaction also like savepoints?
What will be the behavior if someone declares a savepoint with this
name ("_internal_nested_xact"). Will this interfere with this new
functionality? Have we tested scenarios like that?
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Wed, 2 Nov 2022 at 03:52, Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Oct 31, 2022 at 6:54 PM Simon Riggs
<simon.riggs@enterprisedb.com> wrote:What is the behavior if "nested_transactions" value is changed within
a transaction execution, suppose the value was on and we have created
a few levels of nested subtransactions and within the same transaction
I switched it to off or to outer?I think you missed the above comment?
[copy of earlier reply]
Patch does the same dance as with other xact variables.
XactNesting is the value within the transaction and in the patch this
is not exported, so cannot be set externally.
XactNesting is set at transaction start to the variable
DefaultXactNesting, which is set by the GUC.
So its not a problem, but thanks for checking.
I am not sure whether it is good to not allow PREPARE or we can just
prepare it and come out of the complete nested transaction. Suppose
we have multiple savepoints and we say prepare then it will just
succeed so why does it have to be different here?I'm happy to discuss what the behavior should be in this case. It is
not a common case,
and people don't put PREPARE in their scripts except maybe in a test.My reasoning for this code is that we don't want to accept a COMMIT
until we reach top-level of nesting,
so the behavior should be similar for PREPARE, which is just
first-half of final commit.Yeah this is not a very common case. And we can see opinions from
others as well. But I think your reasoning for doing it this way also
makes sense to me.I have some more comments for 0002 1. + if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0) + { + /* Throw ERROR */ + ereport(ERROR, + (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--))); + }I did not understand in case of 'outer' if we are giving rollback from
inner nesting level why it is throwing error? Documentation just says
this[1] but it did not
mention the error. I agree that we might need to give the rollback as
many times as the nesting level but giving errors seems confusing to
me.
Docs mention ROLLBACK at any level will abort the transaction, which
is what the ERROR does.
[1] + <para> + A setting of <quote>outer</quote> will cause a nested + <command>BEGIN</command> to be remembered, so that an equal number + of <command>COMMIT</command> or <command>ROLLBACK</command> commands + are required to end the nesting. In that case a <command>ROLLBACK</command> + at any level will abort the entire outer transaction. + Once we reach the top-level transaction, + the final <command>COMMIT</command> will end the transaction. + This ensures that all commands within the outer transaction are atomic. + </para>2.
+ if (XactNesting == XACT_NEST_OUTER) + { + if (XactNestingLevel <= 0) + s->blockState = TBLOCK_END; + else + ereport(NOTICE, + (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), + errmsg("nested COMMIT, level %u", XactNestingLevel))); + XactNestingLevel--; + return true; + }
This is decrementing the nesting level for XACT_NEST_OUTER,
until we reach the top level, when the commit is allowed.
+ while (s->parent != NULL && !found_subxact) { + if (XactNesting == XACT_NEST_ALL && + XactNestingLevel > 0 && + PointerIsValid(s->name) && + strcmp(s->name, NESTED_XACT_NAME) == 0) + found_subxact = true; + if (s->blockState == TBLOCK_SUBINPROGRESS) s->blockState = TBLOCK_SUBCOMMITI think these changes should be explained in the comments.
This locates the correct subxact by name, as you mention in (4)
3.
+ if (XactNesting == XACT_NEST_OUTER) + { + if (XactNestingLevel > 0) + { + ereport(NOTICE, + (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel))); + XactNestingLevel--; + return false; + } + }Better to write this as if (XactNesting == XACT_NEST_OUTER &&
XactNestingLevel > 0) instead of two levels nested if conditions.
Sure. I had been aiming for clarity.
4. + if (XactNesting == XACT_NEST_ALL && + XactNestingLevel > 0 && + PointerIsValid(s->name) && + strcmp(s->name, NESTED_XACT_NAME) == 0) + found_subxact = true;I think this strcmp(s->name, NESTED_XACT_NAME) is done because there
could be other types of internal subtransaction also like savepoints?
In XACT_NEST_ALL mode, each nested subxact that is created needs a name.
The name is used to ensure we roll back to the correct subxact, which
might exist.
What will be the behavior if someone declares a savepoint with this
name ("_internal_nested_xact"). Will this interfere with this new
functionality?
Clearly! I guess you are saying we should disallow them.
Have we tested scenarios like that?
No, but that can be done.
--
Simon Riggs http://www.EnterpriseDB.com/
On Wed, 2 Nov 2022 at 07:40, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
What will be the behavior if someone declares a savepoint with this
name ("_internal_nested_xact"). Will this interfere with this new
functionality?Clearly! I guess you are saying we should disallow them.
Have we tested scenarios like that?
No, but that can be done.
More tests as requested, plus minor code rework, plus comment updates.
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
002_nested_xacts.v10.patchapplication/octet-stream; name=002_nested_xacts.v10.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 559eb898a9..87e205d32d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8880,6 +8880,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once the top-level of nesting is reached,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 72af656060..50ccb0daca 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -894,3 +894,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fd5103a78e..1f0c3ad42e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,12 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2055,8 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3000,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3012,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
}
void
@@ -3012,6 +3021,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
}
@@ -3182,7 +3192,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3215,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3765,8 +3778,33 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ else
+ ereport(WARNING,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("there is already a transaction in progress")));
+ break;
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
@@ -3815,6 +3853,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3863,6 +3905,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3871,7 +3914,23 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ /*
+ * Don't respond to a COMMIT until we hit the top level
+ * of nesting.
+ */
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else
+ s->blockState = TBLOCK_END;
result = true;
break;
@@ -3900,6 +3959,14 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER &&
+ XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3908,8 +3975,19 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3917,7 +3995,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3931,8 +4028,19 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3942,6 +4050,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4022,6 +4142,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4031,7 +4152,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4041,7 +4169,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4050,8 +4190,19 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4061,6 +4212,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4204,6 +4367,11 @@ DefineSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot define savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
case TBLOCK_INPROGRESS:
@@ -4291,6 +4459,11 @@ ReleaseSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot release savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
/*
@@ -4400,6 +4573,11 @@ RollbackToSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot rollback to savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 05ab087934..1ddf1d9792 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -4533,6 +4540,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 868d21c351..8e1e737d5d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,6 +687,8 @@
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
+#nested_transactions = 'off'
+#rollback_on_commit = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..1cdc697fe1 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,15 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +156,7 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..fffedf49f4 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,287 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+INSERT INTO abc VALUES (2);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK TO SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+RELEASE SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..35ce4ec6ae 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,169 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+SAVEPOINT _internal_nested_xact;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK TO SAVEPOINT _internal_nested_xact;
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+RELEASE SAVEPOINT _internal_nested_xact;
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
003_rollback_on_commit.v1.patchapplication/octet-stream; name=003_rollback_on_commit.v1.patchDownload
commit f5cd3751d0d747ab5d7c0689ac8c0818c07b8580
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date: Wed Oct 26 15:47:22 2022 +0100
Rollback on commit
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bea1d4fc1a..bce7b847d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8846,6 +8846,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c049b49f5..9790dec0df 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,9 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
int DefaultXactNesting;
int XactNesting = XACT_NEST_OFF;
int XactNestingLevel = 0;
@@ -2057,6 +2060,7 @@ StartTransaction(void)
XactIsoLevel = DefaultXactIsoLevel;
XactNesting = DefaultXactNesting;
XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -3013,6 +3017,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3022,6 +3027,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -3915,18 +3921,26 @@ EndTransactionBlock(bool chain)
case TBLOCK_INPROGRESS:
if (XactNesting == XACT_NEST_OUTER)
{
- if (XactNestingLevel <= 0)
- s->blockState = TBLOCK_END;
- else
+ if (XactNestingLevel > 0)
+ {
ereport(NOTICE,
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
XactNestingLevel--;
- return true;
+ }
+ if (XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
}
else
+ {
s->blockState = TBLOCK_END;
- result = true;
+ result = true;
+ }
break;
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1ddf1d9792..86d7f5e134 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1527,6 +1527,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index 1cdc697fe1..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT int XactIsoLevel;
extern PGDLLIMPORT int DefaultXactNesting;
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -157,6 +159,7 @@ typedef struct SavedTransactionCharacteristics
bool save_XactReadOnly;
bool save_XactDeferrable;
int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index cf153a3393..a30c869fc8 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1370,6 +1370,17 @@ SELECT * FROM abc ORDER BY 1;
(0 rows)
RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 4df2d4d382..e6f71d1fad 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -740,6 +740,16 @@ SELECT * FROM abc ORDER BY 1;
RESET nested_transactions;
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
On Mon, 7 Nov 2022 at 14:25, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
On Wed, 2 Nov 2022 at 07:40, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
What will be the behavior if someone declares a savepoint with this
name ("_internal_nested_xact"). Will this interfere with this new
functionality?Clearly! I guess you are saying we should disallow them.
Have we tested scenarios like that?
No, but that can be done.
More tests as requested, plus minor code rework, plus comment updates.
New versions
--
Simon Riggs http://www.EnterpriseDB.com/
Attachments:
001_psql_parse_only.v1.patchapplication/octet-stream; name=001_psql_parse_only.v1.patchDownload
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 864f195992..2ba511d46f 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1105,7 +1105,7 @@ SendQuery(const char *query)
on_error_rollback_savepoint = true;
}
- if (pset.gdesc_flag)
+ if (pset.gdesc_flag || pset.parse_only)
{
/* Describe query's result columns, without executing it */
OK = DescribeQuery(query, &elapsed_msec);
@@ -1285,6 +1285,19 @@ DescribeQuery(const char *query, double *elapsed_msec)
}
PQclear(result);
+ if (pset.parse_only)
+ {
+ /*
+ * If you wanted to, here would be a good place to output a message.
+ * By normal practice, success returns nothing, not even a
+ * command tag, to differentiate from normal execution.
+ *
+ fprintf(pset.queryFout,
+ _("Parsed successfully.\n"));
+ */
+ return true;
+ }
+
result = PQdescribePrepared(pset.db, "");
OK = AcceptResult(result, true) &&
(PQresultStatus(result) == PGRES_COMMAND_OK);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..da6c0454cf 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -112,6 +112,7 @@ typedef struct _psqlSettings
uint64 stmt_lineno; /* line number inside the current statement */
bool timing; /* enable timing of all queries */
+ bool parse_only; /* enable parse_only mode */
FILE *logfile; /* session log file handle */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..caeb83bfac 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -520,6 +520,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
{"csv", no_argument, NULL, 2},
+ {"parse-only", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -713,6 +714,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
case 2:
pset.popt.topt.format = PRINT_CSV;
break;
+ case 3:
+ pset.parse_only = true;
+ break;
default:
unknown_option:
/* getopt_long already emitted a complaint */
002_nested_xacts.v11.patchapplication/octet-stream; name=002_nested_xacts.v11.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 24b1624bad..6ad22a26f8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8884,6 +8884,51 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-default-transaction-nesting" xreflabel="default_transaction_nesting">
+ <term><varname>nested_transactions</varname> (<type>enum</type>)
+ <indexterm>
+ <primary>nested transactions</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>nested_transactions</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls the behavior of transactions when
+ <command>BEGIN</command>, <command>COMMIT</command> or
+ <command>ROLLBACK</command> are received when already in a transaction
+ block.
+ </para>
+ <para>
+ The default is <quote>off</quote>, indicating
+ that when a transaction block is already in progress, a
+ <command>BEGIN</command> will throw a <literal>WARNING</literal>
+ but otherwise do nothing. This is the historic behavior of
+ <productname>PostgreSQL</productname>.
+ </para>
+ <para>
+ A setting of <quote>all</quote> will cause a nested
+ <command>BEGIN</command> to start a subtransaction, which will end when
+ a <command>COMMIT</command> or <command>ROLLBACK</command> is
+ received. In that case a <command>ROLLBACK</command> will only
+ abort the subtransaction. Once the top-level of nesting is reached,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that commands at each transaction nesting level are atomic.
+ </para>
+ <para>
+ A setting of <quote>outer</quote> will cause a nested
+ <command>BEGIN</command> to be remembered, so that an equal number
+ of <command>COMMIT</command> or <command>ROLLBACK</command> commands
+ are required to end the nesting. In that case a <command>ROLLBACK</command>
+ at any level will abort the entire outer transaction.
+ Once we reach the top-level transaction,
+ the final <command>COMMIT</command> will end the transaction.
+ This ensures that all commands within the outer transaction are atomic.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-transaction-isolation" xreflabel="transaction_isolation">
<term><varname>transaction_isolation</varname> (<type>enum</type>)
<indexterm>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 22c8ae9755..53d9e893b3 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -911,3 +911,79 @@ yet simplifies emulation of subtransactions considerably.
Further details on locking mechanics in recovery are given in comments
with the Lock rmgr code.
+
+Nested Transactions
+-------------------
+
+Nested BEGIN/COMMIT statements can be confusing for developers. In this
+example, the first BEGIN and the first COMMIT match, leaving the commands
+between the first and second COMMIT outside of a transaction block,
+allowing them to commit separately from each other. The second BEGIN and
+second COMMIT throw WARNING messages but if an ERROR occurs, the result
+is not atomic and can be hard to manually undo the damage.
+
+BEGIN
+INSERT
+BEGIN
+WARNING: there is already a transaction in progress
+INSERT
+COMMIT
+INSERT
+ERROR: syntax error
+COMMIT
+WARNING: there is no transaction in progress
+SELECT
+val
+---
+1
+2
+(2 rows)
+
+Using SET nested_transactions = 'outer', we get simpler and more understandable
+behavior, with all commands now within the outer transaction block.
+In this example, the ERROR causes rollback of all commands because nested
+begin and commit commands are ignored, while we track the nesting level.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: nested BEGIN, level 1
+INSERT
+COMMIT
+NOTICE: nested COMMIT, level 1
+INSERT
+ERROR: syntax error
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+Notices are shown, so that developers can be certain of the behavior.
+
+Nested COMMIT/ROLLBACK affects all subtransactions under the transaction
+that issued BEGIN, irrespective of subsequent SAVEPOINT/ROLLBACK/RELEASE.
+Nesting can occur to any level.
+
+An alternative approach is to use SET nested_transactions = 'all',
+which changes nested BEGINs into subtransactions and COMMITs into subcommits.
+This allows us to trap nested errors and yet continue the main transaction.
+In this example, a nested ERROR aborts the nested subtransaction, but not
+the main transaction.
+
+BEGIN
+INSERT
+BEGIN
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT
+ERROR: syntax error
+COMMIT
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT
+ROLLBACK
+SELECT
+val
+--
+(0 rows)
+
+The default behavior is equivalent to using SET nested_transactions = 'off'.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8086b857b9..c5852bc7ef 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,12 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+int DefaultXactNesting;
+int XactNesting = XACT_NEST_OFF;
+int XactNestingLevel = 0;
+
+#define NESTED_XACT_NAME "_internal_nested_xact"
+
int synchronous_commit = SYNCHRONOUS_COMMIT_ON;
/*
@@ -2049,6 +2055,8 @@ StartTransaction(void)
}
XactDeferrable = DefaultXactDeferrable;
XactIsoLevel = DefaultXactIsoLevel;
+ XactNesting = DefaultXactNesting;
+ XactNestingLevel = 0;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -2992,7 +3000,7 @@ StartTransactionCommand(void)
/*
* Simple system for saving and restoring transaction characteristics
- * (isolation level, read only, deferrable). We need this for transaction
+ * (isolation level, read only, deferrable, nesting). We need this for transaction
* chaining, so that we can set the characteristics of the new transaction to
* be the same as the previous one. (We need something like this because the
* GUC system resets the characteristics at transaction end, so for example
@@ -3004,6 +3012,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactIsoLevel = XactIsoLevel;
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
+ s->save_XactNesting = XactNesting;
}
void
@@ -3012,6 +3021,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactIsoLevel = s->save_XactIsoLevel;
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
+ XactNesting = s->save_XactNesting;
}
@@ -3182,7 +3192,10 @@ CommitTransactionCommand(void)
CommitSubTransaction();
s = CurrentTransactionState; /* changed by pop */
} while (s->blockState == TBLOCK_SUBCOMMIT);
- /* If we had a COMMIT command, finish off the main xact too */
+ /*
+ * If we had a COMMIT command, finish off the main xact too,
+ * unless this is a nested transaction.
+ */
if (s->blockState == TBLOCK_END)
{
Assert(s->parent == NULL);
@@ -3202,7 +3215,7 @@ CommitTransactionCommand(void)
PrepareTransaction();
s->blockState = TBLOCK_DEFAULT;
}
- else
+ else if (XactNesting == XACT_NEST_OFF)
elog(ERROR, "CommitTransactionCommand: unexpected state %s",
BlockStateAsString(s->blockState));
break;
@@ -3773,8 +3786,33 @@ BeginTransactionBlock(void)
* Already a transaction block in progress.
*/
case TBLOCK_INPROGRESS:
- case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_SUBINPROGRESS:
+ if (XactNesting == XACT_NEST_ALL)
+ {
+ /*
+ * BEGIN starts a nested subtransaction.
+ */
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errmsg("BEGIN starts nested subtransaction, level %u", XactNestingLevel)));
+ BeginInternalSubTransaction(NESTED_XACT_NAME);
+ break;
+ }
+ else if (XactNesting == XACT_NEST_OUTER)
+ {
+ XactNestingLevel++;
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested BEGIN, level %u", XactNestingLevel)));
+ break;
+ }
+ else
+ ereport(WARNING,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("there is already a transaction in progress")));
+ break;
+
+ case TBLOCK_PARALLEL_INPROGRESS:
case TBLOCK_ABORT:
case TBLOCK_SUBABORT:
ereport(WARNING,
@@ -3823,6 +3861,10 @@ PrepareTransactionBlock(const char *gid)
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
+ /* Don't allow prepare until we are back to an unnested state at level 0 */
+ if (XactNestingLevel > 0)
+ return false;
+
/* If successful, change outer tblock state to PREPARE */
if (result)
{
@@ -3871,6 +3913,7 @@ EndTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
bool result = false;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -3879,7 +3922,23 @@ EndTransactionBlock(bool chain)
* to COMMIT.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ /*
+ * Don't respond to a COMMIT until we hit the top level
+ * of nesting.
+ */
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_END;
+ else
+ ereport(NOTICE,
+ (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
+ errmsg("nested COMMIT, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else
+ s->blockState = TBLOCK_END;
result = true;
break;
@@ -3908,6 +3967,14 @@ EndTransactionBlock(bool chain)
* CommitTransactionCommand it's time to exit the block.
*/
case TBLOCK_ABORT:
+ if (XactNesting == XACT_NEST_OUTER &&
+ XactNestingLevel > 0)
+ {
+ ereport(NOTICE,
+ (errmsg("nested COMMIT, level %u in aborted transaction", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
s->blockState = TBLOCK_ABORT_END;
break;
@@ -3916,8 +3983,19 @@ EndTransactionBlock(bool chain)
* open subtransactions and then commit the main transaction.
*/
case TBLOCK_SUBINPROGRESS:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBCOMMIT;
else
@@ -3925,7 +4003,26 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
- if (s->blockState == TBLOCK_INPROGRESS)
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (s->parent == NULL && !found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ /*
+ * With transaction nesting, the COMMIT command no
+ * longer forces main transaction abort, only the
+ * subcommit of subxacts up to the last nested BEGIN.
+ * We only mark state here; CommitTransactionCommand()
+ * will release portals and resources.
+ */
+ ereport(NOTICE,
+ (errmsg("COMMIT will commit nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return true;
+ }
+ else if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_END;
else
elog(FATAL, "EndTransactionBlock: unexpected state %s",
@@ -3939,8 +4036,19 @@ EndTransactionBlock(bool chain)
* transaction.
*/
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -3950,6 +4058,18 @@ EndTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("COMMIT will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return false;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4030,6 +4150,7 @@ void
UserAbortTransactionBlock(bool chain)
{
TransactionState s = CurrentTransactionState;
+ bool found_subxact = false;
switch (s->blockState)
{
@@ -4039,7 +4160,14 @@ UserAbortTransactionBlock(bool chain)
* exit the transaction block.
*/
case TBLOCK_INPROGRESS:
- s->blockState = TBLOCK_ABORT_PENDING;
+ if (XactNesting == XACT_NEST_OUTER && XactNestingLevel > 0)
+ {
+ /* Throw ERROR */
+ ereport(ERROR,
+ (errmsg("nested ROLLBACK, level %u aborts outer transaction", XactNestingLevel--)));
+ }
+ else
+ s->blockState = TBLOCK_ABORT_PENDING;
break;
/*
@@ -4049,7 +4177,19 @@ UserAbortTransactionBlock(bool chain)
* idle state.
*/
case TBLOCK_ABORT:
- s->blockState = TBLOCK_ABORT_END;
+ if (XactNesting == XACT_NEST_OUTER)
+ {
+ if (XactNestingLevel <= 0)
+ s->blockState = TBLOCK_ABORT_END;
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("nested ROLLBACK, level %u in aborted transaction", XactNestingLevel--)));
+ }
+ return;
+ }
+ else
+ s->blockState = TBLOCK_ABORT_END;
break;
/*
@@ -4058,8 +4198,19 @@ UserAbortTransactionBlock(bool chain)
*/
case TBLOCK_SUBINPROGRESS:
case TBLOCK_SUBABORT:
- while (s->parent != NULL)
+ while (s->parent != NULL && !found_subxact)
{
+ /*
+ * If we are handling nested transactions, release all
+ * subtransactions up to the last nested subtransaction,
+ * located via its internal name.
+ */
+ if (XactNesting == XACT_NEST_ALL &&
+ XactNestingLevel > 0 &&
+ PointerIsValid(s->name) &&
+ strcmp(s->name, NESTED_XACT_NAME) == 0)
+ found_subxact = true;
+
if (s->blockState == TBLOCK_SUBINPROGRESS)
s->blockState = TBLOCK_SUBABORT_PENDING;
else if (s->blockState == TBLOCK_SUBABORT)
@@ -4069,6 +4220,18 @@ UserAbortTransactionBlock(bool chain)
BlockStateAsString(s->blockState));
s = s->parent;
}
+ if (XactNesting == XACT_NEST_ALL && XactNestingLevel > 0)
+ {
+ if (!found_subxact)
+ ereport(ERROR,
+ (errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
+ errmsg("nested transaction does not exist")));
+
+ ereport(NOTICE,
+ (errmsg("ROLLBACK will rollback nested subtransaction, level %u", XactNestingLevel)));
+ XactNestingLevel--;
+ return;
+ }
if (s->blockState == TBLOCK_INPROGRESS)
s->blockState = TBLOCK_ABORT_PENDING;
else if (s->blockState == TBLOCK_ABORT)
@@ -4212,6 +4375,11 @@ DefineSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot define savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
case TBLOCK_INPROGRESS:
@@ -4299,6 +4467,11 @@ ReleaseSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot release savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
/*
@@ -4408,6 +4581,11 @@ RollbackToSavepoint(const char *name)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot rollback to savepoints during a parallel operation")));
+ if (name && strcmp(name, NESTED_XACT_NAME) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot reference savepoint with name %s", NESTED_XACT_NAME)));
+
switch (s->blockState)
{
/*
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 349dd6a537..865013f759 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -195,6 +195,13 @@ static const struct config_enum_entry isolation_level_options[] = {
{NULL, 0}
};
+static const struct config_enum_entry xact_nesting_options[] = {
+ {"off", XACT_NEST_OFF, false},
+ {"all", XACT_NEST_ALL, false},
+ {"outer", XACT_NEST_OUTER, false},
+ {NULL, 0, false}
+};
+
static const struct config_enum_entry session_replication_role_options[] = {
{"origin", SESSION_REPLICATION_ROLE_ORIGIN, false},
{"replica", SESSION_REPLICATION_ROLE_REPLICA, false},
@@ -4551,6 +4558,16 @@ struct config_enum ConfigureNamesEnum[] =
NULL, NULL, NULL
},
+ {
+ {"nested_transactions", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets the transaction nesting behavior for each new transaction."),
+ NULL
+ },
+ &DefaultXactNesting,
+ XACT_NEST_OFF, xact_nesting_options,
+ NULL, NULL, NULL
+ },
+
{
{"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the transaction isolation level of each new transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 868d21c351..8e1e737d5d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,6 +687,8 @@
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
+#nested_transactions = 'off'
+#rollback_on_commit = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index c604ee11f8..1cdc697fe1 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -41,6 +41,15 @@
extern PGDLLIMPORT int DefaultXactIsoLevel;
extern PGDLLIMPORT int XactIsoLevel;
+/*
+ * Xact nesting
+ */
+#define XACT_NEST_OFF 0
+#define XACT_NEST_ALL 1
+#define XACT_NEST_OUTER 2
+
+extern PGDLLIMPORT int DefaultXactNesting;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -147,6 +156,7 @@ typedef struct SavedTransactionCharacteristics
int save_XactIsoLevel;
bool save_XactReadOnly;
bool save_XactDeferrable;
+ int save_XactNesting;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..fffedf49f4 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1147,6 +1147,287 @@ SELECT * FROM abc ORDER BY 1;
17
(3 rows)
+SET nested_transactions = 'off';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+WARNING: there is already a transaction in progress
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+WARNING: there is no transaction in progress
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 3
+(1 row)
+
+SET nested_transactions = 'all';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+NOTICE: ROLLBACK will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: COMMIT will commit nested subtransaction, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+INSERT INTO abc VALUES (2);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK TO SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: BEGIN starts nested subtransaction, level 1
+INSERT INTO abc VALUES (2);
+RELEASE SAVEPOINT _internal_nested_xact;
+ERROR: cannot reference savepoint with name _internal_nested_xact
+COMMIT;
+NOTICE: COMMIT will rollback nested subtransaction, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 3
+(2 rows)
+
+SET nested_transactions = 'outer';
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+ERROR: nested ROLLBACK, level 1 aborts outer transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2);
+COMMIT;
+NOTICE: nested COMMIT, level 1
+INSERT INTO abc VALUES (3))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (3)));
+ ^
+INSERT INTO abc VALUES (4);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+NOTICE: nested BEGIN, level 1
+INSERT INTO abc VALUES (2))); --error
+ERROR: syntax error at or near ")"
+LINE 1: INSERT INTO abc VALUES (2)));
+ ^
+COMMIT;
+NOTICE: nested COMMIT, level 1 in aborted transaction
+INSERT INTO abc VALUES (3);
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET nested_transactions;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 7ee5f6aaa5..35ce4ec6ae 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -611,8 +611,169 @@ RESET default_transaction_isolation;
SELECT * FROM abc ORDER BY 1;
-DROP TABLE abc;
+SET nested_transactions = 'off';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'all';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+SAVEPOINT _internal_nested_xact;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK TO SAVEPOINT _internal_nested_xact;
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+RELEASE SAVEPOINT _internal_nested_xact;
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+SET nested_transactions = 'outer';
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+ROLLBACK;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2);
+COMMIT;
+INSERT INTO abc VALUES (3))); --error
+INSERT INTO abc VALUES (4);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+BEGIN;
+INSERT INTO abc VALUES (2))); --error
+COMMIT;
+INSERT INTO abc VALUES (3);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET nested_transactions;
+
+DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
003_rollback_on_commit.v2.patchapplication/octet-stream; name=003_rollback_on_commit.v2.patchDownload
commit 18516b98dc050110df739b6f7aa9d5ef99b5fda5
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date: Tue Nov 22 15:35:47 2022 +0000
rollback_on_commit.v2.patch
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6ad22a26f8..4dbfd63d01 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8850,6 +8850,24 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-rollback-on-commit" xreflabel="default_rollback_on_commit">
+ <term><varname>default_rollback_on_commit</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary>rollback on commit</primary>
+ </indexterm>
+ <indexterm>
+ <primary><varname>rollback_on_commit</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter controls whether a <command>COMMIT</command>
+ will rollback each transaction (<literal>on</literal>), or
+ commit each transaction. The default is <literal>off</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-default-transaction-deferrable" xreflabel="default_transaction_deferrable">
<term><varname>default_transaction_deferrable</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index c5852bc7ef..9562681ff7 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -83,6 +83,9 @@ bool XactReadOnly;
bool DefaultXactDeferrable = false;
bool XactDeferrable;
+bool DefaultXactRollbackOnCommit = false;
+bool XactRollbackOnCommit;
+
int DefaultXactNesting;
int XactNesting = XACT_NEST_OFF;
int XactNestingLevel = 0;
@@ -2057,6 +2060,7 @@ StartTransaction(void)
XactIsoLevel = DefaultXactIsoLevel;
XactNesting = DefaultXactNesting;
XactNestingLevel = 0;
+ XactRollbackOnCommit = DefaultXactRollbackOnCommit;
forceSyncCommit = false;
MyXactFlags = 0;
@@ -3013,6 +3017,7 @@ SaveTransactionCharacteristics(SavedTransactionCharacteristics *s)
s->save_XactReadOnly = XactReadOnly;
s->save_XactDeferrable = XactDeferrable;
s->save_XactNesting = XactNesting;
+ s->save_XactRollbackOnCommit = XactRollbackOnCommit;
}
void
@@ -3022,6 +3027,7 @@ RestoreTransactionCharacteristics(const SavedTransactionCharacteristics *s)
XactReadOnly = s->save_XactReadOnly;
XactDeferrable = s->save_XactDeferrable;
XactNesting = s->save_XactNesting;
+ XactRollbackOnCommit = s->save_XactRollbackOnCommit;
}
@@ -4137,6 +4143,12 @@ EndTransactionBlock(bool chain)
s->chain = chain;
+ if (s->blockState == TBLOCK_END && XactRollbackOnCommit)
+ {
+ s->blockState = TBLOCK_ABORT_PENDING;
+ result = false;
+ }
+
return result;
}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 865013f759..975e149ff8 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1553,6 +1553,15 @@ struct config_bool ConfigureNamesBool[] =
false,
check_transaction_deferrable, NULL, NULL
},
+ {
+ {"rollback_on_commit", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Whether to rollback the current transaction when a COMMIT statement is issued."),
+ NULL
+ },
+ &DefaultXactRollbackOnCommit,
+ false,
+ NULL, NULL, NULL
+ },
{
{"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Enable row security."),
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index 1cdc697fe1..acecc6a161 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT int XactIsoLevel;
extern PGDLLIMPORT int DefaultXactNesting;
+extern PGDLLIMPORT bool DefaultXactRollbackOnCommit;
+
/*
* We implement three isolation levels internally.
* The two stronger ones use one snapshot per database transaction;
@@ -157,6 +159,7 @@ typedef struct SavedTransactionCharacteristics
bool save_XactReadOnly;
bool save_XactDeferrable;
int save_XactNesting;
+ bool save_XactRollbackOnCommit;
} SavedTransactionCharacteristics;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index fffedf49f4..6c92125d07 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1428,6 +1428,17 @@ SELECT * FROM abc ORDER BY 1;
(0 rows)
RESET nested_transactions;
+SET rollback_on_commit = true;
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+ a
+---
+(0 rows)
+
+RESET rollback_on_commit;
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
-- THIS MUST BE THE LAST TEST IN THIS FILE.
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 35ce4ec6ae..325488f6da 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -773,6 +773,16 @@ SELECT * FROM abc ORDER BY 1;
RESET nested_transactions;
+SET rollback_on_commit = true;
+
+TRUNCATE abc;
+BEGIN;
+INSERT INTO abc VALUES (1);
+COMMIT;
+SELECT * FROM abc ORDER BY 1;
+
+RESET rollback_on_commit;
+
DROP TABLE abc;
-- Test for successful cleanup of an aborted transaction at session exit.
On Thu, 27 Oct 2022 at 07:10, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
In the past, developers have wondered how we can provide "--dry-run"
functionality
That would be an awesome functionality, indeed. I have concerns of how
feasible it is in general but I think providing features to allow
developers to build it for their use cases is a good approach. The
corner cases that might not be possible in general might be tractable
developers willing to constrain their development environment or use
information available outside Postgres.
But... I have concerns about some of the design here.
* psql --parse-only
Checks the syntax of all SQL in a script, but without actually
executing it. This is very important in the early stages of complex
migrations because we need to see if the code would generate syntax
errors before we attempt to execute it. When there are many
dependencies between objects, actual execution fails very quickly if
we run in a single transaction, yet running outside of a transaction
can leave a difficult cleanup task. Fixing errors iteratively is
difficult when there are long chains of dependencies between objects,
since there is no easy way to predict how long it will take to make
everything work unless you understand how many syntax errors exist in
the script.
001_psql_parse_only.v1.patch
This effectively enables \gdesc mode for every query. It needs docs
explaining what's actually going to happen and how to use it because
that wasn't super obvious to me even after reading the patch. I'm not
sure reusing DescribeQuery() and then returning early is the best
idea.
But more importantly it's only going to handle the simplest scripts
that don't do DDL that further statements will depend on. That at
least needs to be documented.
* nested transactions = off (default) | all | on
Handle nested BEGIN/COMMIT, which can cause chaos on failure. This is
an important part of guaranteeing that everything that gets executed
is part of a single atomic transaction, which can then be rolled back
- this is a pre-requisite for the last feature.
002_nested_xacts.v7.patch
The default behavior is unchanged (off)
Setting "all" treats nested BEGIN/COMMIT as subtransactions, allowing
some parts to fail without rolling back the outer transaction.
Setting "outer" flattens nested BEGIN/COMMIT into one single outer
transaction, so that any failure rolls back the entire transaction.
I think we've been burned pretty badly by GUCs that control SQL
semantics before. I think there was discussion at the time nested
transactions went in and there must have been a reason we did
SAVEPOINT rather than make nested BEGINs do things like this. But
regardless if we do want to change what nested BEGINs do I think we
have to decide what behaviour we want, think about the backwards
compatibility impacts, and make the change. We can't make it just for
some people some of the time based on a GUC. Doing that makes it
impossible to write scripts that work consistently.
I'm not clear what happens if you have this feature enabled and *also*
use SAVEPOINTs...
You say this is a prerequisite for 003 and I see how they're related
though I don't immediately see why it should be necessary to change
nested BEGIN behaviour to make that work.
* rollback_on_commit = off (default) | on
Force transactions to fail their final commit, ensuring that no
lasting change is made when a script is tested. i.e. accept COMMIT,
but do rollback instead.
003_rollback_on_commit.v1.patch
I suppose technically this is also a "semantics controlled by a GUC"
but I guess it's safe since you would only set this when you want this
debugging environment and then you really do want it to be global.
I'm not sure it's super safe though. Like, dblink connections can
break it, what happens if it gets turned off midway through a
transaction?
I wonder if this should be handled by the client itself the way
autocommit is. Like have an "autocommit" mode of "autorollback"
instead. That would mean having to add it to every client library of
course but then perhaps it would be able to change client behaviour in
ways that make sense at the same time.
--
greg
Greg Stark <stark@mit.edu> writes:
On Thu, 27 Oct 2022 at 07:10, Simon Riggs <simon.riggs@enterprisedb.com> wrote:
* nested transactions = off (default) | all | on
Handle nested BEGIN/COMMIT, which can cause chaos on failure.
I think we've been burned pretty badly by GUCs that control SQL
semantics before.
Yeah, this idea is an absolute nonstarter. rollback_on_commit seems
excessively dangerous as well compared to the value.
I think there was discussion at the time nested
transactions went in and there must have been a reason we did
SAVEPOINT rather than make nested BEGINs do things like this.
I believe the reason was "because the SQL standard says so".
I'm not sure if any of these proposals are still live now that
Simon's retired. Presumably somebody else would have to push
them forward for there to be a chance of anything happening.
regards, tom lane