From 1d27df433b891cdb5a155daa954bd96350f545b8 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 14 Apr 2026 07:27:02 -0300 Subject: [PATCH] Add optional IN AUTONOMOUS TRANSACTION clause to USING statement --- doc/sql.extensions/README.using_statement.md | 34 +++-- src/dsql/StmtNodes.cpp | 137 +++++++++++++++++-- src/dsql/StmtNodes.h | 17 ++- src/dsql/parse.y | 19 ++- src/include/firebird/impl/msg/dsql.h | 2 +- src/include/gen/Firebird.pas | 2 +- 6 files changed, 178 insertions(+), 33 deletions(-) diff --git a/doc/sql.extensions/README.using_statement.md b/doc/sql.extensions/README.using_statement.md index 6dcb395c202..15631cb178f 100644 --- a/doc/sql.extensions/README.using_statement.md +++ b/doc/sql.extensions/README.using_statement.md @@ -10,24 +10,29 @@ input parameter in multiple places), the developer is currently forced to explic more tediously, all output fields. The `USING` statement simplifies this workflow. It provides the ability to declare parameters, sub-routines and -variables while allowing the engine to infer outputs automatically from the contained SQL command. +variables, optionally execute the `DO` command in an autonomous transaction, and still allow the engine to infer +outputs automatically from the contained SQL command. ## Syntax ```sql USING [ ( ) ] [ ] + [ IN AUTONOMOUS TRANSACTION ] DO ``` -**Note:** At least one of `` or `` must be present. A `USING DO ...` statement -without parameters and without local declarations is invalid. +**Note:** At least one of ``, `` or `IN AUTONOMOUS TRANSACTION` must be +present. A `USING DO ...` statement without parameters, without local declarations and without `IN AUTONOMOUS +TRANSACTION` is invalid. ### Components * **``**: A strictly typed list of parameters. These can be bound to values using the `?` placeholder. * **``**: Standard PSQL local declarations (variables, sub-functions and sub-procedures). +* **`IN AUTONOMOUS TRANSACTION`**: Executes the `DO` command in a separate autonomous transaction, reusing the same + semantics already supported by PSQL. * **``**: The DSQL statement to execute. Supported statements include: * `SELECT` * `INSERT` (with or without `RETURNING`) @@ -121,12 +126,21 @@ begin end ``` +### 5. Autonomous Transaction + +```sql +using in autonomous transaction +do insert into audit_log (log_time, message) + values (current_timestamp, 'Entry written in autonomous transaction'); +``` + ## Comparison -| Feature | Standard DSQL | `EXECUTE BLOCK` | `USING` | -| :---------------------- | :------------------------------ | :-------------------------------------------- | :-------------------------------------------------------- | -| **Local Declarations** | No | Yes | Yes | -| **Input Declarations** | Implicit (Positional) | Explicit | Hybrid (implicit and explicit) | -| **Output Declarations** | Inferred | Explicit (`RETURNS`) | Inferred | -| **Verbosity** | Low | High | Medium | -| **Use Case** | Simple queries | Complex logic, loops, no result set inference | Reusing params, variables, sub-routines, standard queries | +| Feature | Standard DSQL | `EXECUTE BLOCK` | `USING` | +| :------------------------- | :------------------------------ | :-------------------------------------------- | :-------------------------------------------------------- | +| **Local Declarations** | No | Yes | Yes | +| **Input Declarations** | Implicit (Positional) | Explicit | Hybrid (implicit and explicit) | +| **Output Declarations** | Inferred | Explicit (`RETURNS`) | Inferred | +| **Verbosity** | Low | High | Medium | +| **Use Case** | Simple queries | Complex logic, loops, no result set inference | Reusing params, variables, sub-routines, standard queries | +| **Autonomous transaction** | No | Yes (without `SUSPEND`) | Yes | diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index dd11eab52df..6b4fca8a07e 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -2410,6 +2410,9 @@ void EraseNode::genBlr(DsqlCompilerScratch* dsqlScratch) GEN_port(dsqlScratch, dsqlScratch->recordKeyMessage); std::optional tableNumber; + const bool useInternalAutoTrans = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && + dsqlReturning && !dsqlScratch->isPsql() && dsqlCursorName.isEmpty(); const bool skipLocked = dsqlRse && dsqlRse->hasSkipLocked(); @@ -2430,6 +2433,13 @@ void EraseNode::genBlr(DsqlCompilerScratch* dsqlScratch) } } + if (useInternalAutoTrans) + { + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); + dsqlScratch->appendUChar(blr_begin); + } + if (dsqlRse) { dsqlScratch->appendUChar(blr_for); @@ -2460,6 +2470,9 @@ void EraseNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (!dsqlScratch->isPsql() && dsqlCursorName.isEmpty()) { + if (useInternalAutoTrans) + dsqlScratch->appendUChar(blr_end); + dsqlGenReturningLocalTableCursor(dsqlScratch, dsqlReturning, tableNumber.value()); if (!skipLocked) @@ -4713,16 +4726,12 @@ DmlNode* InAutonomousTransactionNode::parse(thread_db* tdbb, MemoryPool& pool, C InAutonomousTransactionNode* InAutonomousTransactionNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { - const bool autoTrans = dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK; - dsqlScratch->flags |= DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK; + AutoSetRestoreFlag autoTrans(&dsqlScratch->flags, DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK, true); InAutonomousTransactionNode* node = FB_NEW_POOL(dsqlScratch->getPool()) InAutonomousTransactionNode( dsqlScratch->getPool()); node->action = action->dsqlPass(dsqlScratch); - if (!autoTrans) - dsqlScratch->flags &= ~DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK; - return node; } @@ -4737,9 +4746,33 @@ string InAutonomousTransactionNode::internalPrint(NodePrinter& printer) const void InAutonomousTransactionNode::genBlr(DsqlCompilerScratch* dsqlScratch) { - dsqlScratch->appendUChar(blr_auto_trans); - dsqlScratch->appendUChar(0); // to extend syntax in the future - action->genBlr(dsqlScratch); + bool useInternalAutoTrans = false; + + if (dsqlScratch && !dsqlScratch->isPsql()) + { + if (const auto modifyNode = nodeAs(action)) + useInternalAutoTrans = modifyNode->dsqlReturning && modifyNode->dsqlCursorName.isEmpty(); + else if (const auto eraseNode = nodeAs(action)) + useInternalAutoTrans = eraseNode->dsqlReturning && eraseNode->dsqlCursorName.isEmpty(); + else if (const auto storeNode = nodeAs(action)) + useInternalAutoTrans = storeNode->dsqlReturning != nullptr; + else if (const auto updateOrInsertNode = nodeAs(action)) + useInternalAutoTrans = updateOrInsertNode->returning != nullptr; + else if (const auto mergeNode = nodeAs(action)) + useInternalAutoTrans = mergeNode->returning != nullptr; + } + + if (useInternalAutoTrans) + { + AutoSetRestoreFlag autoTrans(&dsqlScratch->flags, DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK, true); + action->genBlr(dsqlScratch); + } + else + { + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); // to extend syntax in the future + action->genBlr(dsqlScratch); + } } InAutonomousTransactionNode* InAutonomousTransactionNode::pass1(thread_db* tdbb, CompilerScratch* csb) @@ -7246,6 +7279,9 @@ string MergeNode::internalPrint(NodePrinter& printer) const void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) { std::optional tableNumber; + const bool useInternalAutoTrans = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && + returning && !dsqlScratch->isPsql(); if (returning && !dsqlScratch->isPsql()) { @@ -7255,6 +7291,13 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlGenReturningLocalTableDecl(dsqlScratch, tableNumber.value()); } + if (useInternalAutoTrans) + { + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); + dsqlScratch->appendUChar(blr_begin); + } + // Put src info for blr_for. if (hasLineColumn) dsqlScratch->putDebugSrcInfo(line, column); @@ -7493,6 +7536,9 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_end); } + if (useInternalAutoTrans) + dsqlScratch->appendUChar(blr_end); + if (returning && !dsqlScratch->isPsql()) { dsqlGenReturningLocalTableCursor(dsqlScratch, returning, tableNumber.value()); @@ -7949,6 +7995,15 @@ string ModifyNode::internalPrint(NodePrinter& printer) const void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) { + const bool useInternalAutoTrans = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && + dsqlReturning && !dsqlScratch->isPsql() && dsqlCursorName.isEmpty() && + dsqlReturningLocalTableNumber.has_value() && + !(dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT); + const bool deferUpdateOrInsertLocalTableDecl = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT) && + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK); + if (dsqlScratch->recordKeyMessage) { GEN_port(dsqlScratch, dsqlScratch->recordKeyMessage); @@ -7957,7 +8012,10 @@ void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (dsqlReturning && !dsqlScratch->isPsql()) { if (dsqlCursorName.isEmpty()) - dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturningLocalTableNumber.value()); + { + if (!deferUpdateOrInsertLocalTableDecl) + dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturningLocalTableNumber.value()); + } else { dsqlScratch->appendUChar(blr_send); @@ -7965,6 +8023,13 @@ void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) } } + if (useInternalAutoTrans) + { + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); + dsqlScratch->appendUChar(blr_begin); + } + if (dsqlRse) { dsqlScratch->appendUChar(blr_for); @@ -8002,6 +8067,9 @@ void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) !(dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT) && dsqlCursorName.isEmpty()) { + if (useInternalAutoTrans) + dsqlScratch->appendUChar(blr_end); + dsqlGenReturningLocalTableCursor(dsqlScratch, dsqlReturning, dsqlReturningLocalTableNumber.value()); } } @@ -9002,6 +9070,11 @@ string StoreNode::internalPrint(NodePrinter& printer) const void StoreNode::genBlr(DsqlCompilerScratch* dsqlScratch) { + const bool useInternalAutoTrans = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && + dsqlReturning && !dsqlScratch->isPsql() && dsqlReturningLocalTableNumber.has_value() && + !(dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT); + if (dsqlReturning && !dsqlScratch->isPsql()) { if (dsqlRse) @@ -9013,6 +9086,13 @@ void StoreNode::genBlr(DsqlCompilerScratch* dsqlScratch) } } + if (useInternalAutoTrans) + { + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); + dsqlScratch->appendUChar(blr_begin); + } + if (dsqlRse) { dsqlScratch->appendUChar(blr_for); @@ -9035,10 +9115,18 @@ void StoreNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (dsqlReturningLocalTableNumber.has_value()) { + const bool deferUpdateOrInsertCursor = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT) && + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK); + if (dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT) dsqlScratch->appendUChar(blr_end); // close blr_if (blr_eql, blr_internal_info) - dsqlGenReturningLocalTableCursor(dsqlScratch, dsqlReturning, dsqlReturningLocalTableNumber.value()); + if (useInternalAutoTrans) + dsqlScratch->appendUChar(blr_end); + + if (!deferUpdateOrInsertCursor) + dsqlGenReturningLocalTableCursor(dsqlScratch, dsqlReturning, dsqlReturningLocalTableNumber.value()); } } else if (overrideClause.has_value()) @@ -10745,6 +10833,19 @@ string UpdateOrInsertNode::internalPrint(NodePrinter& printer) const void UpdateOrInsertNode::genBlr(DsqlCompilerScratch* dsqlScratch) { + const bool useInternalAutoTrans = + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && + storeNode->dsqlReturningLocalTableNumber.has_value() && !dsqlScratch->isPsql(); + + if (useInternalAutoTrans) + { + dsqlGenReturningLocalTableDecl(dsqlScratch, storeNode->dsqlReturningLocalTableNumber.value()); + + dsqlScratch->appendUChar(blr_auto_trans); + dsqlScratch->appendUChar(0); + dsqlScratch->appendUChar(blr_begin); + } + dsqlScratch->appendUChar(blr_begin); for (auto& varAssign : varAssignments) @@ -10777,6 +10878,13 @@ void UpdateOrInsertNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_end); // blr_if dsqlScratch->appendUChar(blr_end); + + if (useInternalAutoTrans) + { + dsqlScratch->appendUChar(blr_end); + dsqlGenReturningLocalTableCursor(dsqlScratch, storeNode->dsqlReturning, + storeNode->dsqlReturningLocalTableNumber.value()); + } } @@ -10959,14 +11067,15 @@ void UserSavepointNode::execute(thread_db* tdbb, DsqlRequest* request, jrd_tra** StmtNode* UsingNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { - // USING without parameters and subroutines is useless - if (parameters.isEmpty() && !localDeclList) + // USING without parameters, subroutines or autonomous transaction is useless + if (parameters.isEmpty() && !localDeclList && !inAutonomousTransaction) { ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-104) << - Arg::Gds(isc_dsql_using_requires_params_subroutines)); + Arg::Gds(isc_dsql_using_statement_must_contain_clause)); } dsqlScratch->flags |= DsqlCompilerScratch::FLAG_USING_STATEMENT; + dsqlScratch->reserveInitialVarNumbers(parameters.getCount()); const auto statement = dsqlScratch->getDsqlStatement(); unsigned index = 0; @@ -10974,6 +11083,7 @@ StmtNode* UsingNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) const auto node = FB_NEW_POOL(dsqlScratch->getPool()) UsingNode(dsqlScratch->getPool()); node->parameters = parameters; + node->inAutonomousTransaction = inAutonomousTransaction; for (auto newParam : node->parameters) { @@ -11036,6 +11146,7 @@ string UsingNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, parameters); NODE_PRINT(printer, localDeclList); NODE_PRINT(printer, body); + NODE_PRINT(printer, inAutonomousTransaction); return "UsingNode"; } diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index 901224784ff..2ddd8281d6d 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -1544,6 +1544,14 @@ class SavepointEncloseNode final : public TypedNode(pool), + statement(stmt) + { + } + +public: Firebird::string internalPrint(NodePrinter& printer) const override; SavepointEncloseNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; void genBlr(DsqlCompilerScratch* dsqlScratch) override; @@ -1553,13 +1561,7 @@ class SavepointEncloseNode final : public TypedNode(pool), - statement(stmt) - { - } - +public: NestConst statement; }; @@ -2116,6 +2118,7 @@ class UsingNode final : public TypedNode Firebird::Array> parameters; NestConst localDeclList; NestConst body; + bool inAutonomousTransaction = false; }; diff --git a/src/dsql/parse.y b/src/dsql/parse.y index a8b4786d33d..3848b56f1b9 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -4188,16 +4188,33 @@ using { $$ = newNode(); } block_input_params(NOTRIAL(&$2->parameters)) local_declarations_opt + using_autonomous_opt DO using_dml_statement { const auto node = $2; node->localDeclList = $4; - node->body = $6; + node->inAutonomousTransaction = $5; + + if ($5) + { + const auto autoNode = newNode(); + autoNode->action = $7; + node->body = autoNode; + } + else + node->body = $7; + $$ = node; } ; +%type using_autonomous_opt +using_autonomous_opt + : /* nothing */ { $$ = false; } + | IN AUTONOMOUS TRANSACTION { $$ = true; } + ; + %type using_dml_statement using_dml_statement : call { $$ = $1; } diff --git a/src/include/firebird/impl/msg/dsql.h b/src/include/firebird/impl/msg/dsql.h index 22acfc895ef..94c249bdbb4 100644 --- a/src/include/firebird/impl/msg/dsql.h +++ b/src/include/firebird/impl/msg/dsql.h @@ -39,4 +39,4 @@ FB_IMPL_MSG(DSQL, 39, dsql_wrong_param_num, -313, "07", "001", "Wrong number of FB_IMPL_MSG(DSQL, 40, dsql_invalid_drop_ss_clause, -817, "42", "000", "Invalid DROP SQL SECURITY clause") FB_IMPL_MSG(DSQL, 41, upd_ins_cannot_default, -313, "42", "000", "UPDATE OR INSERT value for field @1, part of the implicit or explicit MATCHING clause, cannot be DEFAULT") FB_IMPL_MSG(DSQL, 42, dsql_ltt_invalid_reference, -607, "42", "000", "LOCAL TEMPORARY TABLE @1 cannot be referenced in @2") -FB_IMPL_MSG(DSQL, 43, dsql_using_requires_params_subroutines, -104, "42", "000", "USING requires parameters or subroutines") +FB_IMPL_MSG(DSQL, 43, dsql_using_statement_must_contain_clause, -104, "42", "000", "USING statement must contain at least one clause") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 245040fd90a..6101824a9db 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -6031,7 +6031,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_dsql_invalid_drop_ss_clause = 336003112; isc_upd_ins_cannot_default = 336003113; isc_dsql_ltt_invalid_reference = 336003114; - isc_dsql_using_requires_params_subroutines = 336003115; + isc_dsql_using_statement_must_contain_clause = 336003115; isc_dyn_filter_not_found = 336068645; isc_dyn_func_not_found = 336068649; isc_dyn_index_not_found = 336068656;