diff --git a/doc/README.using_statement.md b/doc/README.using_statement.md new file mode 100644 index 00000000000..ea85fbea48b --- /dev/null +++ b/doc/README.using_statement.md @@ -0,0 +1,132 @@ +# The `USING` Statement + +## Overview + +The `USING` statement is a new DSQL extension designed to bridge the gap between standard DSQL statements and the +powerful but verbose `EXECUTE BLOCK`. + +When adapting a standard DSQL command to use `EXECUTE BLOCK` (for instance, to utilize sub-routines or reuse a single +input parameter in multiple places), the developer is currently forced to explicitly declare all input parameters and, +more tediously, all output fields. + +The `USING` statement simplifies this workflow. It provides the ability to declare parameters and sub-routines while +allowing the engine to infer outputs automatically from the contained SQL command. + +## Syntax + +```sql +USING [ ( ) ] + [ ] +DO +``` + +**Note:** At least one of `` or `` must be present. A `USING ... DO` statement +without parameters and without subroutines is invalid. + +### Components + +* **``**: A strictly typed list of parameters. These can be bound to values using the `?` + placeholder. +* **``**: Standard PSQL function or procedure declarations. +* **``**: The DSQL statement to execute. Supported statements include: + * `SELECT` + * `INSERT` (with or without `RETURNING`) + * `UPDATE` (with or without `RETURNING`) + * `UPDATE OR INSERT` (with or without `RETURNING`) + * `DELETE` (with or without `RETURNING`) + * `MERGE` (with or without `RETURNING`) + * `CALL` + * `EXECUTE PROCEDURE` + +## Key Features + +1. **Inferred Outputs**: Unlike `EXECUTE BLOCK`, you do not need to explicitly declare a `RETURNS (...)` clause. The + output columns are automatically inferred from the `` in the `DO` clause. +2. **Statement Type Transparency**: The API returns the statement type of the inner `` (e.g., if the + inner command is a `SELECT`, the client sees a `SELECT` statement). +3. **Parameter Reuse**: Input parameters declared in the `USING` clause can be used multiple times within the script + using named references (e.g., `:p1`), while only requiring a single bind from the client application. +4. **Mixed Parameter Binding**: You can mix declared parameters (bound via `?` in the declaration) and direct + positional parameters (using `?` inside the `DO` command). + +## Examples + +### 1. Basic Parameter Reuse and Subroutines + +This example demonstrates declaring typed parameters, defining local functions/procedures, and using them in a query. + +```sql +using (p1 integer = ?, p2 integer = ?) + -- Declare a local function + declare function subfunc (i1 integer) returns integer + as + begin + return i1; + end + + -- Declare a local procedure + declare procedure subproc (i1 integer) returns (o1 integer) + as + begin + o1 = i1; + suspend; + end +do +-- The main query +select subfunc(:p1) + o1 + from subproc(:p2 + ?) +``` + +In this scenario: +1. The client binds values to `p1` and `p2`. +2. The client binds a third value to the `?` inside the `DO` clause. +3. The result set structure is inferred from the `SELECT` statement. + +### 2. Simplifying Parameter Reuse + +Without `USING`, inserting the same bind value into multiple columns requires sending the data twice. + +**Standard DSQL:** +```sql +insert into generic_table (col_a, col_b) values (?, ?); +-- Client must bind: [100, 100] +``` + +**With `USING`:** +```sql +using (val integer = ?) +do insert into generic_table (col_a, col_b) values (:val, :val); +-- Client binds: [100] +``` + +### 3. EXECUTE STATEMENT with Named Parameters + +```sql +execute block returns (ri integer, rs varchar(20)) +as +begin + execute statement ('using(i int = ?, s varchar(20) = ?) do select :i, :s from rdb$database') (s := '2', i := 1) into ri, rs; + suspend; +end +``` + +### 4. EXECUTE STATEMENT with Unnamed Parameters + +```sql +execute block returns (ri integer, rs varchar(20)) +as +begin + execute statement ('using(i int = ?, s varchar(20) = ?) do select :i, :s from rdb$database') (1, '2') into ri, rs; + suspend; +end +``` + +## Comparison + +| Feature | Standard DSQL | `EXECUTE BLOCK` | `USING` | +| :---------------------- | :------------------------------ | :-------------------------------------------- | :-------------------------------------------- | +| **Subroutines** | 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, subroutines, standard queries | diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index a285d62be6d..937970513e4 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -2494,7 +2494,10 @@ void CreateAlterFunctionNode::compile(thread_db* /*tdbb*/, DsqlCompilerScratch* dsqlScratch->setPsql(true); if (localDeclList) + { + localDeclList = localDeclList->dsqlPass(dsqlScratch); localDeclList->genBlr(dsqlScratch); + } dsqlScratch->loopLevel = 0; dsqlScratch->cursorNumber = 0; @@ -3410,7 +3413,10 @@ void CreateAlterProcedureNode::compile(thread_db* /*tdbb*/, DsqlCompilerScratch* dsqlScratch->setPsql(true); if (localDeclList) + { + localDeclList = localDeclList->dsqlPass(dsqlScratch); localDeclList->genBlr(dsqlScratch); + } dsqlScratch->loopLevel = 0; dsqlScratch->cursorNumber = 0; @@ -3965,7 +3971,10 @@ void CreateAlterTriggerNode::compile(thread_db* /*tdbb*/, DsqlCompilerScratch* d dsqlScratch->setPsql(true); if (localDeclList) + { + localDeclList = localDeclList->dsqlPass(dsqlScratch); localDeclList->genBlr(dsqlScratch); + } dsqlScratch->loopLevel = 0; dsqlScratch->cursorNumber = 0; diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index f63aa70dd4b..42eb0d1e008 100644 --- a/src/dsql/DsqlCompilerScratch.h +++ b/src/dsql/DsqlCompilerScratch.h @@ -77,6 +77,7 @@ class DsqlCompilerScratch : public BlrDebugWriter static const unsigned FLAG_VIEW_WITH_CHECK = 0x8000; static const unsigned FLAG_EXEC_BLOCK = 0x010000; static const unsigned FLAG_ALLOW_LTT_REFERENCES = 0x020000; + static const unsigned FLAG_USING_STATEMENT = 0x040000; static const unsigned MAX_NESTING = 512; diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 7d74b33b55f..da2e423be07 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -14176,9 +14176,10 @@ void VariableNode::genBlr(DsqlCompilerScratch* dsqlScratch) { auto varScratch = outerDecl ? dsqlScratch->mainScratch : dsqlScratch; - const bool execBlock = (varScratch->flags & DsqlCompilerScratch::FLAG_EXEC_BLOCK); + const bool execBlockOrUsing = (varScratch->flags & + (DsqlCompilerScratch::FLAG_EXEC_BLOCK | DsqlCompilerScratch::FLAG_USING_STATEMENT)); - if (dsqlVar->type == dsql_var::TYPE_INPUT && !execBlock) + if (dsqlVar->type == dsql_var::TYPE_INPUT && !execBlockOrUsing) { dsqlScratch->appendUChar(blr_parameter2); @@ -14196,7 +14197,7 @@ void VariableNode::genBlr(DsqlCompilerScratch* dsqlScratch) } else { - // If this is an EXECUTE BLOCK input parameter, use the internal variable. + // If this is an EXECUTE BLOCK or USING input parameter, use the internal variable. dsqlScratch->appendUChar(blr_variable); if (outerDecl) diff --git a/src/dsql/Nodes.h b/src/dsql/Nodes.h index 50a82b0452b..c7bd567d127 100644 --- a/src/dsql/Nodes.h +++ b/src/dsql/Nodes.h @@ -1495,6 +1495,7 @@ class StmtNode : public DmlNode TYPE_SUSPEND, TYPE_TRUNCATE_LOCAL_TABLE, TYPE_UPDATE_OR_INSERT, + TYPE_USING, TYPE_EXT_INIT_PARAMETERS, TYPE_EXT_TRIGGER diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index 7f084ad3d05..088f6b353f2 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -110,6 +110,7 @@ static void preModifyEraseTriggers(thread_db* tdbb, TrigVector** trigs, static void preprocessAssignments(thread_db* tdbb, CompilerScratch* csb, StreamType stream, CompoundStmtNode* compoundNode, const std::optional* insertOverride); static void restartRequest(const Request* request, jrd_tra* transaction); +static void revertParametersOrder(Array& parameters); static void validateExpressions(thread_db* tdbb, const Array& validations); } // namespace Jrd @@ -1279,8 +1280,6 @@ DeclareCursorNode* DeclareCursorNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) cursorNumber = dsqlScratch->cursorNumber++; dsqlScratch->cursors.push(this); - dsqlScratch->putDebugDeclaredCursor(cursorNumber, dsqlName); - ++dsqlScratch->scopeLevel; return this; @@ -1304,6 +1303,8 @@ string DeclareCursorNode::internalPrint(NodePrinter& printer) const void DeclareCursorNode::genBlr(DsqlCompilerScratch* dsqlScratch) { + dsqlScratch->putDebugDeclaredCursor(cursorNumber, dsqlName); + dsqlScratch->appendUChar(blr_dcl_cursor); dsqlScratch->appendUShort(cursorNumber); @@ -1665,7 +1666,8 @@ DeclareSubFuncNode* DeclareSubFuncNode::dsqlPass(DsqlCompilerScratch* dsqlScratc dsqlFunction->udf_name.object = name; fb_assert(dsqlBlock->returns.getCount() == 1); - const auto returnType = dsqlBlock->returns[0]->type; + auto returnType = dsqlBlock->returns[0]->type; + returnType->resolve(dsqlScratch); dsqlFunction->udf_dtype = returnType->dtype; dsqlFunction->udf_scale = returnType->scale; @@ -1689,6 +1691,8 @@ DeclareSubFuncNode* DeclareSubFuncNode::dsqlPass(DsqlCompilerScratch* dsqlScratc auto param = *i; const unsigned paramIndex = i - dsqlBlock->parameters.begin(); + param->type->resolve(dsqlScratch); + SignatureParameter sigParam(pool); sigParam.type = 0; sigParam.number = (SSHORT) dsqlSignature.parameters.getCount(); @@ -2005,6 +2009,8 @@ DeclareSubProcNode* DeclareSubProcNode::dsqlPass(DsqlCompilerScratch* dsqlScratc auto param = *i; const unsigned paramIndex = i - dsqlBlock->parameters.begin(); + param->type->resolve(dsqlScratch); + SignatureParameter sigParam(pool); sigParam.type = 0; // input sigParam.number = (SSHORT) dsqlSignature.parameters.getCount(); @@ -2048,7 +2054,9 @@ DeclareSubProcNode* DeclareSubProcNode::dsqlPass(DsqlCompilerScratch* dsqlScratc for (NestConst* i = dsqlBlock->returns.begin(); i != dsqlBlock->returns.end(); ++i) { - const auto param = *i; + auto param = *i; + + param->type->resolve(dsqlScratch); SignatureParameter sigParam(pool); sigParam.type = 1; // output @@ -2190,8 +2198,15 @@ DmlNode* DeclareVariableNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerS return node; } -DeclareVariableNode* DeclareVariableNode::dsqlPass(DsqlCompilerScratch* /*dsqlScratch*/) +DeclareVariableNode* DeclareVariableNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { + if (dsqlDef->defaultClause) + dsqlDef->defaultClause->value = doDsqlPass(dsqlScratch, dsqlDef->defaultClause->value); + + dsql_fld* field = dsqlDef->type; + dsqlVar = dsqlScratch->makeVariable(field, field->fld_name.c_str(), dsql_var::TYPE_LOCAL, 0, 0); + dsqlVar->initialized = true; + return this; } @@ -4999,20 +5014,30 @@ ExecBlockNode* ExecBlockNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) ExecBlockNode* node = FB_NEW_POOL(dsqlScratch->getPool()) ExecBlockNode(dsqlScratch->getPool()); - for (NestConst* param = parameters.begin(); param != parameters.end(); ++param) + node->parameters = parameters; + + for (auto paramIt = node->parameters.begin(); paramIt != node->parameters.end(); ++paramIt) { + const USHORT index = static_cast(paramIt - node->parameters.begin()); + auto newParam = *paramIt; + PsqlChanger changer(dsqlScratch, false); - node->parameters.add(*param); - ParameterClause* newParam = node->parameters.back(); + newParam->type->fld_id = paramIt - node->parameters.begin(); + + if (!(dsqlScratch->flags & DsqlCompilerScratch::FLAG_SUB_ROUTINE)) + { + newParam->type->resolve(dsqlScratch); + + dsqlScratch->makeVariable(newParam->type, newParam->name.c_str(), + dsql_var::TYPE_INPUT, 0, (USHORT) (2 * index), index); + } newParam->parameterExpr = doDsqlPass(dsqlScratch, newParam->parameterExpr); if (newParam->defaultClause) newParam->defaultClause->value = doDsqlPass(dsqlScratch, newParam->defaultClause->value); - newParam->type->resolve(dsqlScratch); - newParam->type->fld_id = param - parameters.begin(); { // scope ValueExprNode* temp = newParam->parameterExpr; @@ -5027,25 +5052,37 @@ ExecBlockNode* ExecBlockNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) false); } // end scope - if (param != parameters.begin()) + if (paramIt != node->parameters.begin()) node->parameters.end()[-2]->type->fld_next = newParam->type; } node->returns = returns; - for (FB_SIZE_T i = 0; i < node->returns.getCount(); ++i) + for (auto retIt = node->returns.begin(); retIt != node->returns.end(); ++retIt) { - node->returns[i]->type->resolve(dsqlScratch); - node->returns[i]->type->fld_id = i; + const USHORT index = static_cast(retIt - node->returns.begin()); + auto newRet = *retIt; + + newRet->type->fld_id = index; + + if (!(dsqlScratch->flags & DsqlCompilerScratch::FLAG_SUB_ROUTINE)) + { + newRet->type->resolve(dsqlScratch); + + dsqlScratch->makeVariable(newRet->type, newRet->name.c_str(), + dsql_var::TYPE_OUTPUT, 1, (USHORT) (2 * index), parameters.getCount() + index); + } - if (i != 0) - node->returns[i - 1]->type->fld_next = node->returns[i]->type; + if (index != 0) + node->returns[index - 1]->type->fld_next = newRet->type; } - node->localDeclList = localDeclList; - node->body = body; + LocalDeclarationsNode::checkUniqueFieldsNames(localDeclList, ¶meters, &returns); + + if (localDeclList) + node->localDeclList = localDeclList->dsqlPass(dsqlScratch); - LocalDeclarationsNode::checkUniqueFieldsNames(node->localDeclList, ¶meters, &returns); + node->body = body; return node; } @@ -5073,31 +5110,6 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) // Sub routine doesn't need ports and should generate BLR as declared in its metadata. const bool subRoutine = dsqlScratch->flags & DsqlCompilerScratch::FLAG_SUB_ROUTINE; - unsigned returnsPos; - - if (!subRoutine) - { - // Now do the input parameters. - for (FB_SIZE_T i = 0; i < parameters.getCount(); ++i) - { - ParameterClause* parameter = parameters[i]; - - dsqlScratch->makeVariable(parameter->type, parameter->name.c_str(), - dsql_var::TYPE_INPUT, 0, (USHORT) (2 * i), i); - } - - returnsPos = dsqlScratch->variables.getCount(); - - // Now do the output parameters. - for (FB_SIZE_T i = 0; i < returns.getCount(); ++i) - { - ParameterClause* parameter = returns[i]; - - dsqlScratch->makeVariable(parameter->type, parameter->name.c_str(), - dsql_var::TYPE_OUTPUT, 1, (USHORT) (2 * i), parameters.getCount() + i); - } - } - DsqlStatement* const statement = dsqlScratch->getDsqlStatement(); dsqlScratch->appendUChar(blr_begin); @@ -5133,8 +5145,12 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) else statement->setReceiveMsg(nullptr); + unsigned returnsPos; + unsigned inputStart = 0; + if (subRoutine) { + inputStart = dsqlScratch->variables.getCount(); dsqlScratch->genParameters(parameters, returns); returnsPos = dsqlScratch->variables.getCount() - dsqlScratch->outputVariables.getCount(); } @@ -5152,7 +5168,7 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) // This validation is needed only for subroutines. Standard EXECUTE BLOCK moves input // parameters to variables and are then validated. - for (unsigned i = 0; i < returnsPos; ++i) + for (unsigned i = inputStart; i < returnsPos; ++i) { const dsql_var* variable = dsqlScratch->variables[i]; const TypeClause* field = variable->field; @@ -5172,7 +5188,10 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) const auto& variables = subRoutine ? dsqlScratch->outputVariables : dsqlScratch->variables; for (const auto variable : variables) - dsqlScratch->putLocalVariable(variable); + { + if (variable->type != dsql_var::TYPE_LOCAL) + dsqlScratch->putLocalVariable(variable); + } dsqlScratch->setPsql(true); @@ -5211,22 +5230,6 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->endDebug(); } -// Revert parameters order for EXECUTE BLOCK statement -void ExecBlockNode::revertParametersOrder(Array& parameters) -{ - int start = 0; - int end = int(parameters.getCount()) - 1; - - while (start < end) - { - dsql_par* temp = parameters[start]; - parameters[start] = parameters[end]; - parameters[end] = temp; - ++start; - --end; - } -} - //-------------------- @@ -6521,24 +6524,18 @@ void LocalDeclarationsNode::checkUniqueFieldsNames(const LocalDeclarationsNode* } } -void LocalDeclarationsNode::genBlr(DsqlCompilerScratch* dsqlScratch) +LocalDeclarationsNode* LocalDeclarationsNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { - // Sub routine needs a different approach from EXECUTE BLOCK. - // EXECUTE BLOCK needs "ports", which creates DSQL messages using the client charset. - // Sub routine doesn't need ports and should generate BLR as declared in its metadata. - const bool isSubRoutine = dsqlScratch->flags & DsqlCompilerScratch::FLAG_SUB_ROUTINE; - - Array declaredVariables; + PsqlChanger changer(dsqlScratch, true); + const auto node = FB_NEW_POOL(dsqlScratch->getPool()) LocalDeclarationsNode(dsqlScratch->getPool()); const auto end = statements.end(); for (auto ptr = statements.begin(); ptr != end; ++ptr) { - auto parameter = *ptr; - - if (const auto varNode = nodeAs(parameter)) + if (const auto varNode = nodeAs(*ptr)) { - dsql_fld* field = varNode->dsqlDef->type; + const dsql_fld* field = varNode->dsqlDef->type; const NestConst* rest = ptr; while (++rest != end) @@ -6554,37 +6551,55 @@ void LocalDeclarationsNode::genBlr(DsqlCompilerScratch* dsqlScratch) } } } + } + } + + for (auto stmt : statements) + node->statements.add(stmt->dsqlPass(dsqlScratch)); + + return node; +} + +void LocalDeclarationsNode::genBlr(DsqlCompilerScratch* dsqlScratch) +{ + // Sub routine needs a different approach from EXECUTE BLOCK. + // EXECUTE BLOCK needs "ports", which creates DSQL messages using the client charset. + // Sub routine doesn't need ports and should generate BLR as declared in its metadata. + const bool isSubRoutine = dsqlScratch->flags & DsqlCompilerScratch::FLAG_SUB_ROUTINE; - const auto variable = dsqlScratch->makeVariable(field, field->fld_name.c_str(), - dsql_var::TYPE_LOCAL, 0, 0); - declaredVariables.add(variable); + for (auto parameter : statements) + { + if (const auto varNode = nodeAs(parameter)) + { + dsql_var* variable = varNode->dsqlVar; + fb_assert(variable); dsqlScratch->putLocalVariableDecl(variable, varNode, varNode->dsqlDef->type->collate); // Some field attributes are calculated inside putLocalVariable(), so we reinitialize // the descriptor. - DsqlDescMaker::fromField(&variable->desc, field); + DsqlDescMaker::fromField(&variable->desc, variable->field); } else if (nodeIs(parameter) || nodeIs(parameter) || nodeIs(parameter)) { dsqlScratch->putDebugSrcInfo(parameter->line, parameter->column); - parameter->dsqlPass(dsqlScratch); parameter->genBlr(dsqlScratch); } else fb_assert(false); } - auto declVarIt = declaredVariables.begin(); - for (const auto parameter : statements) { if (const auto varNode = nodeAs(parameter)) { + const auto variable = varNode->dsqlVar; + fb_assert(variable); + dsqlScratch->putDebugSrcInfo(parameter->line, parameter->column); - dsqlScratch->putLocalVariableInit(*declVarIt++, varNode); + dsqlScratch->putLocalVariableInit(variable, varNode); } } @@ -9513,7 +9528,7 @@ void SelectNode::genBlr(DsqlCompilerScratch* dsqlScratch) auto message = statement->getSendMsg(); - if (message->msg_parameter) + if (message && message->msg_parameter) GEN_port(dsqlScratch, message); else statement->setSendMsg(nullptr); @@ -10910,6 +10925,171 @@ 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) + { + ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-104) << + Arg::Gds(isc_dsql_using_requires_params_subroutines)); + } + + dsqlScratch->flags |= DsqlCompilerScratch::FLAG_USING_STATEMENT; + + const auto statement = dsqlScratch->getDsqlStatement(); + unsigned index = 0; + + const auto node = FB_NEW_POOL(dsqlScratch->getPool()) UsingNode(dsqlScratch->getPool()); + + node->parameters = parameters; + + for (auto newParam : node->parameters) + { + PsqlChanger changer(dsqlScratch, false); + + newParam->parameterExpr = doDsqlPass(dsqlScratch, newParam->parameterExpr); + + if (newParam->defaultClause) + newParam->defaultClause->value = doDsqlPass(dsqlScratch, newParam->defaultClause->value); + + newParam->type->resolve(dsqlScratch); + newParam->type->fld_id = index; + + { // scope + auto temp = newParam->parameterExpr; + + // Initialize this stack variable, and make it look like a node + dsc descNode; + + newParam->type->flags |= FLD_nullable; + DsqlDescMaker::fromField(&descNode, newParam->type); + PASS1_set_parameter_type(dsqlScratch, temp, + [&] (dsc* desc) { *desc = descNode; }, + false); + + if (const auto paramNode = nodeAs(temp)) + { + if (paramNode->dsqlParameter) + { + paramNode->dsqlParameter->par_name = newParam->name; + paramNode->dsqlParameter->par_alias = newParam->name; + } + } + } + + dsqlScratch->makeVariable(newParam->type, newParam->name.c_str(), + dsql_var::TYPE_INPUT, 0, (USHORT) (2 * index), index); + + ++index; + } + + LocalDeclarationsNode::checkUniqueFieldsNames(localDeclList, ¶meters, nullptr); + + if (localDeclList) + node->localDeclList = localDeclList->dsqlPass(dsqlScratch); + + if (body) + node->body = body->dsqlPass(dsqlScratch); + + if (statement->getSendMsg()) + revertParametersOrder(statement->getSendMsg()->msg_parameters); + + return node; +} + +string UsingNode::internalPrint(NodePrinter& printer) const +{ + StmtNode::internalPrint(printer); + + NODE_PRINT(printer, parameters); + NODE_PRINT(printer, localDeclList); + NODE_PRINT(printer, body); + + return "UsingNode"; +} + +void UsingNode::genBlr(DsqlCompilerScratch* dsqlScratch) +{ + const auto statement = dsqlScratch->getDsqlStatement(); + + // For SELECT-based statement types, GEN_statement does NOT call GEN_port or generate + // blr_receive_batch, so we must handle it here. For other types (INSERT, UPDATE, DELETE), + // GEN_statement already handles the message port generation. + const bool isSelect = (statement->getType() == DsqlStatement::TYPE_SELECT || + statement->getType() == DsqlStatement::TYPE_SELECT_UPD); + + // For SELECT types, check if we have actual parameters. Clear sendMsg if not + // (similar to what GEN_statement does for non-SELECT types). + auto sendMsg = statement->getSendMsg(); + if (isSelect && sendMsg && !sendMsg->msg_parameter) + { + statement->setSendMsg(nullptr); + sendMsg = nullptr; + } + + // For SELECT types with parameters, we need to wrap everything in blr_begin + // and generate our own message/receive + const bool genSelectWrapper = isSelect && sendMsg; + + if (genSelectWrapper) + dsqlScratch->appendUChar(blr_begin); + + // Generate the port (message) BLR for input parameters - only for SELECT statements + if (genSelectWrapper) + GEN_port(dsqlScratch, sendMsg); + + if (dsqlScratch->variables.hasData()) + { + // Generate receive statement to get parameter values - only for SELECT statements + if (genSelectWrapper) + { + dsqlScratch->appendUChar(blr_receive); + dsqlScratch->appendUChar(0); + } + + dsqlScratch->appendUChar(blr_begin); + + for (const auto var : dsqlScratch->variables) + dsqlScratch->putLocalVariable(var); + } + + dsqlScratch->appendUChar(blr_begin); + + if (localDeclList) + localDeclList->genBlr(dsqlScratch); + + dsqlScratch->loopLevel = 0; + + // Temporarily hide SendMsg so the body (SelectNode) doesn't generate duplicate port/receive + if (sendMsg) + statement->setSendMsg(nullptr); + + // The body should be generated in DSQL mode (isPsql=false) so that RETURNING + // generates the correct local table declarations and cursor handling. + PsqlChanger changer(dsqlScratch, false); + + if (body) + body->genBlr(dsqlScratch); + + if (sendMsg) + statement->setSendMsg(sendMsg); + + dsqlScratch->putOuterMaps(); + GEN_hidden_variables(dsqlScratch); + + dsqlScratch->appendUChar(blr_end); + + if (dsqlScratch->variables.hasData()) + dsqlScratch->appendUChar(blr_end); + + if (genSelectWrapper) + dsqlScratch->appendUChar(blr_end); +} + + +//-------------------- + + // Generate a field list that correspond to table fields. template static void dsqlExplodeFields(dsql_rel* relation, Array >& fields, bool includeComputed) @@ -12184,6 +12364,22 @@ static void restartRequest(const Request* request, jrd_tra* transaction) Arg::Gds(isc_concurrent_transaction) << Arg::Int64(top_request->req_conflict_txn)); } +// Revert parameters order for EXECUTE BLOCK and USING statements. +static void revertParametersOrder(Array& parameters) +{ + int start = 0; + int end = int(parameters.getCount()) - 1; + + while (start < end) + { + const auto temp = parameters[start]; + parameters[start] = parameters[end]; + parameters[end] = temp; + ++start; + --end; + } +} + // Execute a list of validation expressions. static void validateExpressions(thread_db* tdbb, const Array& validations) { diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index 4d1d4936a02..8845194c098 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -508,6 +508,7 @@ class DeclareVariableNode final : public TypedNode dsqlDef; dsc varDesc; + dsql_var* dsqlVar = nullptr; USHORT varId = 0; bool usedInSubRoutines = false; }; @@ -785,9 +786,6 @@ class ExecBlockNode final : public TypedNode& parameters); - public: Firebird::Array> parameters; Firebird::Array> returns; @@ -1049,6 +1047,7 @@ class LocalDeclarationsNode final : public TypedNode>* outputParameters); public: + LocalDeclarationsNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; void genBlr(DsqlCompilerScratch* dsqlScratch) override; public: @@ -2100,6 +2099,26 @@ class UpdateOrInsertNode final : public TypedNode +{ +public: + explicit UsingNode(MemoryPool& pool) + : TypedNode(pool), + parameters(pool) + { + } + + Firebird::string internalPrint(NodePrinter& printer) const override; + StmtNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; + void genBlr(DsqlCompilerScratch* dsqlScratch) override; + +public: + Firebird::Array> parameters; + NestConst localDeclList; + NestConst body; +}; + + } // namespace #endif // DSQL_STMT_NODES_H diff --git a/src/dsql/gen.cpp b/src/dsql/gen.cpp index d51a51c70d4..7a89d2236c4 100644 --- a/src/dsql/gen.cpp +++ b/src/dsql/gen.cpp @@ -290,7 +290,7 @@ void GEN_statement(DsqlCompilerScratch* scratch, DmlNode* node) default: { dsql_msg* message = statement->getSendMsg(); - if (!message->msg_parameter) + if (!message || !message->msg_parameter) statement->setSendMsg(NULL); else { diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index 41dbe10bf44..5966b6852d7 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -135 shift/reduce conflicts, 7 reduce/reduce conflicts. +136 shift/reduce conflicts, 7 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 140aa27c903..e603475d5a7 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -852,6 +852,7 @@ using namespace Firebird; Jrd::ExecBlockNode* execBlockNode; Jrd::StoreNode* storeNode; Jrd::UpdateOrInsertNode* updInsNode; + Jrd::UsingNode* usingNode; Jrd::AggNode* aggNode; Jrd::SysFuncCallNode* sysFuncCallNode; Jrd::ValueIfNode* valueIfNode; @@ -928,6 +929,7 @@ dml_statement | select { $$ = $1; } | update { $$ = $1; } | update_or_insert { $$ = $1; } + | using { $$ = $1; } ; %type ddl_statement @@ -4175,6 +4177,36 @@ block_parameter($parameters) } ; +// USING + +%type using +using + : USING + { $$ = newNode(); } + block_input_params(NOTRIAL(&$2->parameters)) + local_declarations_opt + DO + using_dml_statement + { + const auto node = $2; + node->localDeclList = $4; + node->body = $6; + $$ = node; + } + ; + +%type using_dml_statement +using_dml_statement + : call { $$ = $1; } + | delete { $$ = $1; } + | insert { $$ = $1; } + | merge { $$ = $1; } + | exec_procedure { $$ = $1; } + | select { $$ = $1; } + | update { $$ = $1; } + | update_or_insert { $$ = $1; } + ; + // CREATE VIEW %type view_clause diff --git a/src/include/firebird/impl/msg/dsql.h b/src/include/firebird/impl/msg/dsql.h index 80fb197c052..22acfc895ef 100644 --- a/src/include/firebird/impl/msg/dsql.h +++ b/src/include/firebird/impl/msg/dsql.h @@ -39,3 +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") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index f5de43ff25f..0523216bc53 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -6023,6 +6023,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_dyn_filter_not_found = 336068645; isc_dyn_func_not_found = 336068649; isc_dyn_index_not_found = 336068656; diff --git a/src/jrd/PreparedStatement.cpp b/src/jrd/PreparedStatement.cpp index 1a75583fb68..959bc15f613 100644 --- a/src/jrd/PreparedStatement.cpp +++ b/src/jrd/PreparedStatement.cpp @@ -33,6 +33,7 @@ #include "../jrd/Attachment.h" using namespace Firebird; +using namespace Jrd; namespace @@ -46,9 +47,16 @@ namespace } }; - void dscToMetaItem(const dsc* desc, MsgMetadata::Item& item) + void dscToMetaItem(const dsql_par* par, MsgMetadata::Item& item) { item.finished = true; + item.field = par->par_name.c_str(); + item.relation = par->par_rel_name.object.c_str(); + item.owner = par->par_owner_name.c_str(); + item.alias = par->par_alias.c_str(); + item.schema = par->par_rel_name.schema.c_str(); + + const auto desc = &par->par_desc; switch (desc->dsc_dtype) { @@ -546,7 +554,7 @@ void PreparedStatement::parseDsqlMessage(const dsql_msg* dsqlMsg, Array& va msgMetadata->setItemsCount(paramCount); for (FB_SIZE_T i = 0; i < paramCount; ++i) - dscToMetaItem(¶ms[i]->par_desc, msgMetadata->getItem(i)); + dscToMetaItem(params[i], msgMetadata->getItem(i)); msgMetadata->makeOffsets(); msg.resize(msgMetadata->getMessageLength()); diff --git a/src/jrd/extds/ExtDS.cpp b/src/jrd/extds/ExtDS.cpp index bc788ec62e6..5d75aaef4cd 100644 --- a/src/jrd/extds/ExtDS.cpp +++ b/src/jrd/extds/ExtDS.cpp @@ -1824,6 +1824,27 @@ void Statement::prepare(thread_db* tdbb, Transaction* tran, const string& sql, b m_sql = sql; m_sql.trim(); m_preparedByReq = m_callerPrivileges ? tdbb->getRequest() : NULL; + + if (m_sqlParamNames.isEmpty() && getInputs() > 0) + { + fb_assert(m_sqlParamsMap.isEmpty()); + + // Populate parameters from metadata if preprocessing was skipped + + const unsigned count = getInputs(); + const MetaString empty; + + for (unsigned i = 0; i < count; ++i) + { + const MetaString parameterName(getParameterName(i)); + FB_SIZE_T n = 0; + + if (!m_sqlParamNames.find(parameterName, n)) + n = m_sqlParamNames.add(parameterName); + + m_sqlParamsMap.add(&m_sqlParamNames[n]); + } + } } void Statement::setTimeout(thread_db* tdbb, unsigned int timeout) @@ -2236,12 +2257,6 @@ void Statement::setInParams(thread_db* tdbb, const MetaName* const* names, const FB_SIZE_T excCount = in_excess ? in_excess->getCount() : 0; const FB_SIZE_T sqlCount = m_sqlParamNames.getCount(); - if ((m_error = (!names && sqlCount))) - { - // Parameter name expected - ERR_post(Arg::Gds(isc_eds_prm_name_expected)); - } - // OK : count - excCount <= sqlCount <= count // Check if all passed named parameters, not marked as excess, are present in query text @@ -2266,7 +2281,9 @@ void Statement::setInParams(thread_db* tdbb, const MetaName* const* names, } } - if (sqlCount || names && count > 0) + // When names is provided (named parameters), do named matching + // When names is nullptr (unnamed parameters), do positional matching even if SQL has named parameters + if (names && count > 0 && sqlCount) { const unsigned int mapCount = m_sqlParamsMap.getCount(); // Here NestConst plays against its objective. It temporary unconstifies the values. diff --git a/src/jrd/extds/ExtDS.h b/src/jrd/extds/ExtDS.h index cd02c9f965b..8f51e6861fe 100644 --- a/src/jrd/extds/ExtDS.h +++ b/src/jrd/extds/ExtDS.h @@ -726,6 +726,9 @@ class Statement : public Firebird::PermanentStorage void bindToRequest(Jrd::Request* request, Statement** impure); void unBindFromRequest(); +protected: + virtual const char* getParameterName(unsigned index) const { return nullptr; } + protected: virtual void doPrepare(Jrd::thread_db* tdbb, const Firebird::string& sql) = 0; virtual void doSetTimeout(Jrd::thread_db* tdbb, unsigned int timeout) = 0; diff --git a/src/jrd/extds/InternalDS.h b/src/jrd/extds/InternalDS.h index 61b0aa9d811..90f64c6b46b 100644 --- a/src/jrd/extds/InternalDS.h +++ b/src/jrd/extds/InternalDS.h @@ -134,6 +134,17 @@ class InternalStatement : public Statement virtual bool doFetch(Jrd::thread_db* tdbb); virtual void doClose(Jrd::thread_db* tdbb, bool drop); + const char* getParameterName(unsigned index) const override + { + if (m_inMetadata && index < m_inMetadata->getCount()) + { + Firebird::FbLocalStatus status; + return m_inMetadata->getAlias(&status, index); + } + + return nullptr; + } + virtual void putExtBlob(Jrd::thread_db* tdbb, dsc& src, dsc& dst); virtual void getExtBlob(Jrd::thread_db* tdbb, const dsc& src, dsc& dst); diff --git a/src/jrd/extds/IscDS.h b/src/jrd/extds/IscDS.h index 54744bb9c16..ad85daffc04 100644 --- a/src/jrd/extds/IscDS.h +++ b/src/jrd/extds/IscDS.h @@ -590,6 +590,19 @@ class IscStatement : public Statement virtual bool doFetch(Jrd::thread_db* tdbb); virtual void doClose(Jrd::thread_db* tdbb, bool drop); + const char* getParameterName(unsigned index) const override + { + if (m_in_xsqlda && index < m_in_xsqlda->sqld) + { + const auto& var = m_in_xsqlda->sqlvar[index]; + + if (var.sqlname_length > 0) + return var.sqlname; + } + + return nullptr; + } + virtual void doSetInParams(Jrd::thread_db* tdbb, unsigned int count, const Firebird::MetaString* const* names, const NestConst* params);