diff --git a/doc/sql.extensions/README.merge.txt b/doc/sql.extensions/README.merge.txt index 7ab69b7ddf..09bb471888 100644 --- a/doc/sql.extensions/README.merge.txt +++ b/doc/sql.extensions/README.merge.txt @@ -3,7 +3,7 @@ MERGE statement ----------------- Function: - Read data from the source and INSERT or UPDATE in the target table depending on a + Read data from the source and INSERT, UPDATE or DELETE in the target table depending on a condition. Author: @@ -18,23 +18,28 @@ MERGE statement ... [] [] - + [] ::= | - + | + ::= WHEN MATCHED [ AND ] THEN { UPDATE SET | DELETE } - ::= - WHEN NOT MATCHED [ AND ] THEN + ::= + WHEN NOT MATCHED [ BY TARGET ] [ AND ] THEN INSERT [ ] VALUES + ::= + WHEN NOT MATCHED BY SOURCE [ AND ] THEN + { UPDATE SET | DELETE } + Syntax rules: - 1. At least one of or should be specified. + 1. At least one clause should be specified. Scope: DSQL, PSQL @@ -52,14 +57,40 @@ MERGE statement INSERT (id, name) VALUES (cd.id, cd.name) - Notes: - A right join is made between INTO and USING tables using the condition. - UPDATE is called when a record exist in the left table (INTO), otherwise - INSERT is called. + 2. + MERGE + INTO customers c + USING new_customers nc + ON (c.id = nc.id) + WHEN MATCHED THEN + UPDATE SET + name = cd.name + WHEN NOT MATCHED BY SOURCE THEN + DELETE - As soon it's decided if the source matched or not a record in the target, the set of the + Notes: + A join is made between USING and INTO tables. + + The join type depends on the presence of + and : + - + : FULL JOIN + - : RIGHT JOIN + - : LEFT JOIN + - only : INNER JOIN + + As soon it's decided if the source and target has a matching, the set of the corresponding (WHEN MATCHED / WHEN NOT MATCHED) statements is evaluated in the order specified, to check their optional conditions. The first statement which has its condition evaluated to true is the one which will be executed, and the subsequent ones will be ignored. - If no record is returned in the join, INSERT is not called. + If no record is returned in the join, no action will be called. + + is called when a match between source and target exists. + UPDATE or DELETE will change the target table. + + is called when a source record matches no record in target. + INSERT will change the target table. + + is called when a target record matches no record in source. + UPDATE or DELETE will change the target table. + That clause is allowed only since v5. diff --git a/src/common/classes/stack.h b/src/common/classes/stack.h index 03f1e0ad9a..fa15d4bff5 100644 --- a/src/common/classes/stack.h +++ b/src/common/classes/stack.h @@ -140,6 +140,12 @@ namespace Firebird { : FB_NEW_POOL(getPool()) Entry(e, 0); } + void push(const Stack& s) + { + for (const_iterator itr(s); itr.hasData(); ++itr) + push(itr.object()); + } + Object pop() { fb_assert(stk); diff --git a/src/common/keywords.cpp b/src/common/keywords.cpp index 662f2aaf19..5eb003d4a0 100644 --- a/src/common/keywords.cpp +++ b/src/common/keywords.cpp @@ -486,6 +486,7 @@ static const TOK tokens[] = {TOK_TAGS, "TAGS", true}, {TOK_TAN, "TAN", true}, {TOK_TANH, "TANH", true}, + {TOK_TARGET, "TARGET", true}, {TOK_TEMPORARY, "TEMPORARY", true}, {TOK_THEN, "THEN", false}, {TOK_TIES, "TIES", true}, diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 723f9edd0b..631273b6ae 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -8499,14 +8499,32 @@ bool DsqlMapNode::dsqlMatch(DsqlCompilerScratch* dsqlScratch, const ExprNode* ot //-------------------- -DerivedFieldNode::DerivedFieldNode(MemoryPool& pool, const MetaName& aName, USHORT aScope, - ValueExprNode* aValue) - : TypedNode(pool), - name(aName), - value(aValue), - context(NULL), - scope(aScope) +void DerivedFieldNode::getContextNumbers(Firebird::SortedArray& contextNumbers, const DsqlContextStack& contextStack) { + for (DsqlContextStack::const_iterator stack(contextStack); stack.hasData(); ++stack) + { + const auto* const context = stack.object(); + + if (context->ctx_win_maps.hasData()) + { + for (const auto& winMap : context->ctx_win_maps) + { + // bottleneck + fb_assert(winMap->context <= MAX_UCHAR); + + if (!contextNumbers.exist(winMap->context)) + contextNumbers.add(winMap->context); + } + } + else + { + // bottleneck + fb_assert(context->ctx_context <= MAX_UCHAR); + + if (!contextNumbers.exist(context->ctx_context)) + contextNumbers.add(context->ctx_context); + } + } } string DerivedFieldNode::internalPrint(NodePrinter& printer) const @@ -8653,29 +8671,8 @@ void DerivedFieldNode::genBlr(DsqlCompilerScratch* dsqlScratch) { if (context->ctx_main_derived_contexts.hasData()) { - HalfStaticArray derivedContexts; - - for (DsqlContextStack::const_iterator stack(context->ctx_main_derived_contexts); - stack.hasData(); ++stack) - { - const dsql_ctx* const derivedContext = stack.object(); - - if (derivedContext->ctx_win_maps.hasData()) - { - for (auto& winMap : derivedContext->ctx_win_maps) - { - // bottleneck - fb_assert(winMap->context <= MAX_UCHAR); - derivedContexts.add(winMap->context); - } - } - else - { - // bottleneck - fb_assert(derivedContext->ctx_context <= MAX_UCHAR); - derivedContexts.add(derivedContext->ctx_context); - } - } + SortedArray derivedContexts; + getContextNumbers(derivedContexts, context->ctx_main_derived_contexts); const FB_SIZE_T derivedContextsCount = derivedContexts.getCount(); diff --git a/src/dsql/ExprNodes.h b/src/dsql/ExprNodes.h index d4ee42e9b4..f0fb4cb035 100644 --- a/src/dsql/ExprNodes.h +++ b/src/dsql/ExprNodes.h @@ -1058,7 +1058,25 @@ class DerivedFieldNode : public TypedNode(pool), + name(aName), + value(aValue), + context(NULL), + scope(aScope) + { + } + + // Construct already processed node. + DerivedFieldNode(MemoryPool& pool, dsql_ctx* aContext, ValueExprNode* aValue) + : TypedNode(pool), + value(aValue), + context(aContext), + scope(0) + { + } + + static void getContextNumbers(Firebird::SortedArray& contextNumbers, const DsqlContextStack& contextStack); virtual void getChildren(NodeRefsHolder& holder, bool dsql) const { diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index e1b4e2aa84..04d1957167 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -78,7 +78,7 @@ static void dsqlGenReturning(DsqlCompilerScratch* dsqlScratch, ReturningClause* Nullable localTableNumber); static void dsqlGenReturningLocalTableCursor(DsqlCompilerScratch* dsqlScratch, ReturningClause* returning, USHORT localTableNumber); -static void dsqlGenReturningLocalTableDecl(DsqlCompilerScratch* dsqlScratch, ReturningClause* returning, USHORT tableNumber); +static void dsqlGenReturningLocalTableDecl(DsqlCompilerScratch* dsqlScratch, USHORT tableNumber); static dsql_ctx* dsqlGetContext(const RecordSourceNode* node); static void dsqlGetContexts(DsqlContextStack& contexts, const RecordSourceNode* node); static StmtNode* dsqlNullifyReturning(DsqlCompilerScratch*, StmtNode* input); @@ -2365,7 +2365,7 @@ void EraseNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_begin); tableNumber = dsqlScratch->localTableNumber++; - dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturning, tableNumber.value); + dsqlGenReturningLocalTableDecl(dsqlScratch, tableNumber.value); } else { @@ -5508,8 +5508,10 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) join->dsqlFrom->items[0] = source; - // Left join if WHEN NOT MATCHED is present. - if (whenNotMatched.hasData()) + // Choose join type. + if (whenNotMatchedBySource.hasData()) + join->rse_jointype = whenNotMatchedByTarget.hasData() ? blr_full : blr_right; + else if (whenNotMatchedByTarget.hasData()) join->rse_jointype = blr_left; join->dsqlFrom->items[1] = target; @@ -5520,64 +5522,13 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) querySpec->dsqlFrom->items[0] = join; querySpec->rse_plan = plan; - BoolExprNode* matchedConditions = nullptr; - BoolExprNode* notMatchedConditions = nullptr; - - for (auto& matched : whenMatched) - { - if (matched.condition) - matchedConditions = PASS1_compose(matchedConditions, matched.condition, blr_or); - else - { - matchedConditions = nullptr; - break; - } - } - - for (auto& notMatched : whenNotMatched) - { - if (notMatched.condition) - notMatchedConditions = PASS1_compose(notMatchedConditions, notMatched.condition, blr_or); - else - { - notMatchedConditions = nullptr; - break; - } - } - - if (matchedConditions || notMatchedConditions) - { - const char* targetName = target->alias.nullStr(); - if (!targetName) - targetName = target->dsqlName.c_str(); - - if (whenMatched.hasData()) // WHEN MATCHED - { - auto missingNode = FB_NEW_POOL(pool) MissingBoolNode( - pool, FB_NEW_POOL(pool) RecordKeyNode(pool, blr_dbkey, targetName)); - - querySpec->dsqlWhere = FB_NEW_POOL(pool) NotBoolNode(pool, missingNode); - - if (matchedConditions) - querySpec->dsqlWhere = PASS1_compose(querySpec->dsqlWhere, matchedConditions, blr_and); - } - - if (whenNotMatched.hasData()) // WHEN NOT MATCHED - { - BoolExprNode* temp = FB_NEW_POOL(pool) MissingBoolNode(pool, - FB_NEW_POOL(pool) RecordKeyNode(pool, blr_dbkey, targetName)); - - if (notMatchedConditions) - temp = PASS1_compose(temp, notMatchedConditions, blr_and); - - querySpec->dsqlWhere = PASS1_compose(querySpec->dsqlWhere, temp, blr_or); - } - } - const auto selectExpr = FB_NEW_POOL(pool) SelectExprNode(pool); selectExpr->querySpec = querySpec; selectExpr->orderClause = order; + if (returning && dsqlScratch->isPsql()) + selectExpr->dsqlFlags |= RecordSourceNode::DFLAG_SINGLETON; + const auto dsqlSelect = FB_NEW_POOL(pool) SelectNode(pool); dsqlSelect->dsqlExpr = selectExpr; @@ -5589,17 +5540,57 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) source = processedRse->dsqlStreams->items[0]; target = nodeAs(processedRse->dsqlStreams->items[1]); - mergeNode->targetContext = dsqlGetContext(target); + mergeNode->oldContext = dsqlGetContext(target); DsqlContextStack usingCtxs; + + PASS1_expand_contexts(usingCtxs, source->dsqlContext); + DerivedFieldNode::getContextNumbers(mergeNode->usingContexts, usingCtxs); + + usingCtxs.clear(); dsqlGetContexts(usingCtxs, source); + bool useMatchedConditions = whenMatched.hasData(); + bool useNotMatchedByTargetConditions = whenNotMatchedByTarget.hasData(); + bool useNotMatchedBySourceConditions = whenNotMatchedBySource.hasData(); + + for (auto& matched : whenMatched) + { + if (!matched.condition) + { + useMatchedConditions = false; + break; + } + } + + for (auto& notMatched : whenNotMatchedByTarget) + { + if (!notMatched.condition) + { + useNotMatchedByTargetConditions = false; + break; + } + } + + for (auto& notMatched : whenNotMatchedBySource) + { + if (!notMatched.condition) + { + useNotMatchedBySourceConditions = false; + break; + } + } + + BoolExprNode* matchedConditions = nullptr; + BoolExprNode* notMatchedByTargetConditions = nullptr; + BoolExprNode* notMatchedBySourceConditions = nullptr; + for (auto& matched : whenMatched) { auto& processedMatched = mergeNode->whenMatched.add(); processedMatched.assignments = matched.assignments; - if (matched.assignments) + if (matched.assignments) // SET { // Get the assignments of the UPDATE dsqlScratch. // Separate the new and org values to process in correct contexts. @@ -5611,24 +5602,25 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) processedMatched.processedFields.add(assign->asgnTo); } - const auto oldContext = dsqlGetContext(target); + { // scope + // Go to the same level of source and target contexts. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); - ++dsqlScratch->scopeLevel; // Go to the same level of source and target contexts. + dsqlScratch->context->push(usingCtxs); // push the USING contexts + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context - for (DsqlContextStack::iterator itr(usingCtxs); itr.hasData(); ++itr) - dsqlScratch->context->push(itr.object()); // push the USING contexts + processedMatched.condition = doDsqlPass(dsqlScratch, matched.condition, false); - dsqlScratch->context->push(oldContext); // process old context values + if (useMatchedConditions) + { + matchedConditions = PASS1_compose(matchedConditions, + doDsqlPass(dsqlScratch, matched.condition, false), blr_or); + } - processedMatched.condition = doDsqlPass(dsqlScratch, matched.condition, false); - - for (auto& ptr : processedMatched.processedValues) - ptr = doDsqlPass(dsqlScratch, ptr, false); - - // And pop the contexts. - dsqlScratch->context->pop(); - dsqlScratch->context->pop(); - --dsqlScratch->scopeLevel; + for (auto& ptr : processedMatched.processedValues) + ptr = doDsqlPass(dsqlScratch, ptr, false); + } // Process relation. processedMatched.modifyRelation = PASS1_relation(dsqlScratch, relation); @@ -5642,23 +5634,17 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) if (returning) { - // Repush the source contexts. - ++dsqlScratch->scopeLevel; // Go to the same level of source and target contexts. + // Go to the same level of source and target contexts. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); - for (DsqlContextStack::iterator itr(usingCtxs); itr.hasData(); ++itr) - dsqlScratch->context->push(itr.object()); // push the USING contexts + dsqlScratch->context->push(usingCtxs); // push the USING contexts + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context - dsqlScratch->context->push(oldContext); // process old context values - - modContext->ctx_scope_level = oldContext->ctx_scope_level; + modContext->ctx_scope_level = mergeNode->oldContext->ctx_scope_level; mergeNode->returning = processedMatched.processedReturning = dsqlProcessReturning(dsqlScratch, - oldContext, modContext, returning, dsqlScratch->isPsql()); - - // And pop them. - dsqlScratch->context->pop(); - dsqlScratch->context->pop(); - --dsqlScratch->scopeLevel; + mergeNode->oldContext, modContext, returning, dsqlScratch->isPsql()); } auto valueIt = processedMatched.processedValues.begin(); @@ -5674,43 +5660,51 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) // We do not allow cases like UPDATE SET f1 = v1, f2 = v2, f1 = v3... dsqlFieldAppearsOnce(processedMatched.processedFields, "MERGE"); } - else + else // DELETE { - ++dsqlScratch->scopeLevel; // Go to the same level of source and target contexts. + // Go to the same level of source and target contexts. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); - for (DsqlContextStack::iterator itr(usingCtxs); itr.hasData(); ++itr) - dsqlScratch->context->push(itr.object()); // push the USING contexts - - dsqlScratch->context->push(mergeNode->targetContext); // process old context values + dsqlScratch->context->push(usingCtxs); // push the USING contexts + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context processedMatched.condition = doDsqlPass(dsqlScratch, matched.condition, false); - mergeNode->returning = processedMatched.processedReturning = dsqlProcessReturning(dsqlScratch, - mergeNode->targetContext, nullptr, returning, dsqlScratch->isPsql()); + if (useMatchedConditions) + { + matchedConditions = PASS1_compose(matchedConditions, + doDsqlPass(dsqlScratch, matched.condition, false), blr_or); + } - // And pop the contexts. - dsqlScratch->context->pop(); - dsqlScratch->context->pop(); - --dsqlScratch->scopeLevel; + mergeNode->returning = processedMatched.processedReturning = dsqlProcessReturning(dsqlScratch, + mergeNode->oldContext, nullptr, returning, dsqlScratch->isPsql()); } } - for (auto& notMatched : whenNotMatched) + for (auto& notMatched : whenNotMatchedByTarget) { - ++dsqlScratch->scopeLevel; // Go to the same level of the source context. + // Go to the same level of the source context. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); - for (DsqlContextStack::iterator itr(usingCtxs); itr.hasData(); ++itr) - dsqlScratch->context->push(itr.object()); // push the USING contexts + dsqlScratch->context->push(usingCtxs); // push the USING contexts // The INSERT relation should be processed in a higher level than the source context. ++dsqlScratch->scopeLevel; - auto& processedNotMatched = mergeNode->whenNotMatched.add(); + auto& processedNotMatched = mergeNode->whenNotMatchedByTarget.add(); processedNotMatched.overrideClause = notMatched.overrideClause; processedNotMatched.condition = doDsqlPass(dsqlScratch, notMatched.condition, false); + if (useNotMatchedByTargetConditions) + { + notMatchedByTargetConditions = PASS1_compose(notMatchedByTargetConditions, + doDsqlPass(dsqlScratch, notMatched.condition, false), blr_or); + } + { // scope - DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); + DsqlContextStack::AutoRestore autoContextForValues(*dsqlScratch->context); const auto values = doDsqlPass(dsqlScratch, notMatched.values, false); @@ -5808,23 +5802,171 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) if (returning) { - const auto oldContext = dsqlGetContext(target); - dsqlScratch->context->push(oldContext); + dsqlScratch->context->push(mergeNode->oldContext); const auto storeContext = dsqlGetContext(processedNotMatched.storeRelation); - storeContext->ctx_scope_level = oldContext->ctx_scope_level; + storeContext->ctx_scope_level = mergeNode->oldContext->ctx_scope_level; mergeNode->returning = processedNotMatched.processedReturning = dsqlProcessReturning(dsqlScratch, - oldContext, storeContext, returning, dsqlScratch->isPsql()); + mergeNode->oldContext, storeContext, returning, dsqlScratch->isPsql()); dsqlScratch->context->pop(); } - - // Pop the USING context. - dsqlScratch->context->pop(); - --dsqlScratch->scopeLevel; } + for (auto& notMatched : whenNotMatchedBySource) + { + auto& processedNotMatched = mergeNode->whenNotMatchedBySource.add(); + processedNotMatched.assignments = notMatched.assignments; + + if (notMatched.assignments) // SET + { + // Get the assignments of the UPDATE dsqlScratch. + // Separate the new and org values to process in correct contexts. + for (auto& stmt : notMatched.assignments->statements) + { + const auto assign = nodeAs(stmt); + fb_assert(assign); + processedNotMatched.processedValues.add(assign->asgnFrom); + processedNotMatched.processedFields.add(assign->asgnTo); + } + + ++dsqlScratch->scopeLevel; // Go to the same level of source and target contexts. + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context + + processedNotMatched.condition = doDsqlPass(dsqlScratch, notMatched.condition, false); + + if (useNotMatchedBySourceConditions) + { + notMatchedBySourceConditions = PASS1_compose(notMatchedBySourceConditions, + doDsqlPass(dsqlScratch, notMatched.condition, false), blr_or); + } + + for (auto& ptr : processedNotMatched.processedValues) + ptr = doDsqlPass(dsqlScratch, ptr, false); + + // And pop the context. + dsqlScratch->context->pop(); + --dsqlScratch->scopeLevel; + + // Process relation. + processedNotMatched.modifyRelation = PASS1_relation(dsqlScratch, relation); + auto modContext = dsqlGetContext(processedNotMatched.modifyRelation); + + // Process new context values. + for (auto& ptr : processedNotMatched.processedFields) + ptr = doDsqlPass(dsqlScratch, ptr, false); + + dsqlScratch->context->pop(); + + if (returning) + { + // Go to the same level of source and target contexts. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); + + dsqlScratch->context->push(usingCtxs); // push the USING contexts + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context + + modContext->ctx_scope_level = mergeNode->oldContext->ctx_scope_level; + + mergeNode->returning = processedNotMatched.processedReturning = dsqlProcessReturning(dsqlScratch, + mergeNode->oldContext, modContext, returning, dsqlScratch->isPsql()); + } + + auto valueIt = processedNotMatched.processedValues.begin(); + + for (auto& field : processedNotMatched.processedFields) + { + if (!PASS1_set_parameter_type(dsqlScratch, *valueIt, field, false)) + PASS1_set_parameter_type(dsqlScratch, field, *valueIt, false); + + ++valueIt; + } + + // We do not allow cases like UPDATE SET f1 = v1, f2 = v2, f1 = v3... + dsqlFieldAppearsOnce(processedNotMatched.processedFields, "MERGE"); + } + else // DELETE + { + // Go to the same level of source and target contexts. + AutoSetRestore autoScopeLevel(&dsqlScratch->scopeLevel, dsqlScratch->scopeLevel + 1); + DsqlContextStack::AutoRestore autoContext(*dsqlScratch->context); + + dsqlScratch->context->push(mergeNode->oldContext); // push the OLD context + + processedNotMatched.condition = doDsqlPass(dsqlScratch, notMatched.condition, false); + + if (useNotMatchedBySourceConditions) + { + notMatchedBySourceConditions = PASS1_compose(notMatchedBySourceConditions, + doDsqlPass(dsqlScratch, notMatched.condition, false), blr_or); + } + + dsqlScratch->context->push(usingCtxs); // push the USING contexts + + mergeNode->returning = processedNotMatched.processedReturning = dsqlProcessReturning(dsqlScratch, + mergeNode->oldContext, nullptr, returning, dsqlScratch->isPsql()); + } + } + + const auto targetDbKey = [dsqlScratch, mergeNode]() { + auto relNode = FB_NEW_POOL(dsqlScratch->getPool()) RelationSourceNode(dsqlScratch->getPool()); + relNode->dsqlContext = mergeNode->oldContext; + + auto recKeyNode = FB_NEW_POOL(dsqlScratch->getPool()) RecordKeyNode(dsqlScratch->getPool(), blr_dbkey); + recKeyNode->dsqlRelation = relNode; + + return recKeyNode; + }; + + const auto sourceDbKey = [dsqlScratch, source]() { + auto relNode = FB_NEW_POOL(dsqlScratch->getPool()) RelationSourceNode(dsqlScratch->getPool()); + relNode->dsqlContext = source->dsqlContext; + + return FB_NEW_POOL(dsqlScratch->getPool()) DerivedFieldNode(dsqlScratch->getPool(), source->dsqlContext, + MAKE_constant("1", CONSTANT_BOOLEAN)); + }; + + if (join->rse_jointype != blr_inner) + { + if (whenMatched.hasData()) + { + matchedConditions = PASS1_compose( + PASS1_compose( + FB_NEW_POOL(pool) NotBoolNode(pool, FB_NEW_POOL(pool) MissingBoolNode(pool, targetDbKey())), + join->rse_jointype == blr_left ? + nullptr : + FB_NEW_POOL(pool) NotBoolNode(pool, FB_NEW_POOL(pool) MissingBoolNode(pool, sourceDbKey())), + blr_and + ), + matchedConditions, + blr_and); + } + + if (whenNotMatchedByTarget.hasData()) + { + notMatchedByTargetConditions = PASS1_compose( + FB_NEW_POOL(pool) MissingBoolNode(pool, targetDbKey()), + notMatchedByTargetConditions, + blr_and); + } + + if (whenNotMatchedBySource.hasData()) + { + notMatchedBySourceConditions = PASS1_compose( + FB_NEW_POOL(pool) MissingBoolNode(pool, sourceDbKey()), + notMatchedBySourceConditions, + blr_and); + } + } + + fb_assert(!mergeNode->rse->dsqlWhere); + + mergeNode->rse->dsqlWhere = PASS1_compose(mergeNode->rse->dsqlWhere, matchedConditions, blr_or); + mergeNode->rse->dsqlWhere = PASS1_compose(mergeNode->rse->dsqlWhere, notMatchedByTargetConditions, blr_or); + mergeNode->rse->dsqlWhere = PASS1_compose(mergeNode->rse->dsqlWhere, notMatchedBySourceConditions, blr_or); + if (!dsqlScratch->isPsql()) { // Describe it as TYPE_RETURNING_CURSOR if RETURNING is present or as INSERT otherwise. @@ -5845,7 +5987,8 @@ string MergeNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, usingClause); NODE_PRINT(printer, condition); //// FIXME-PRINT: NODE_PRINT(printer, whenMatched); - //// FIXME-PRINT: NODE_PRINT(printer, whenNotMatched); + //// FIXME-PRINT: NODE_PRINT(printer, whenNotMatchedByTarget); + //// FIXME-PRINT: NODE_PRINT(printer, whenNotMatchedBySource); NODE_PRINT(printer, returning); NODE_PRINT(printer, rse); @@ -5861,7 +6004,7 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_begin); tableNumber = dsqlScratch->localTableNumber++; - dsqlGenReturningLocalTableDecl(dsqlScratch, returning, tableNumber.value); + dsqlGenReturningLocalTableDecl(dsqlScratch, tableNumber.value); } // Put src info for blr_for. @@ -5871,34 +6014,117 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) // Generate FOR loop dsqlScratch->appendUChar(blr_for); - dsqlScratch->putBlrMarkers(MARK_FOR_UPDATE | MARK_MERGE); - - if (returning && dsqlScratch->isPsql()) - dsqlScratch->appendUChar(blr_singular); - GEN_rse(dsqlScratch, rse); // Build body of FOR loop - dsqlScratch->appendUChar(blr_if); + if (whenNotMatchedBySource.hasData()) + { + dsqlScratch->appendUChar(blr_if); + dsqlScratch->appendUChar(blr_missing); + dsqlScratch->appendUChar(blr_derived_expr); + dsqlScratch->appendUChar(usingContexts.getCount()); - if (whenNotMatched.isEmpty()) - dsqlScratch->appendUChar(blr_not); + for (auto usingContext : usingContexts) + GEN_stuff_context_number(dsqlScratch, usingContext); - dsqlScratch->appendUChar(blr_missing); - dsqlScratch->appendUChar(blr_dbkey); - GEN_stuff_context(dsqlScratch, targetContext); + dsqlScratch->appendUChar(blr_literal); + dsqlScratch->appendUChar(blr_bool); + dsqlScratch->appendUChar(1); + + for (auto nextNotMatched = whenNotMatchedBySource.begin(), notMatched = nextNotMatched++; + notMatched != whenNotMatchedBySource.end(); + notMatched = nextNotMatched++) + { + const bool isLast = nextNotMatched == whenNotMatchedBySource.end(); + + if (notMatched->condition || !isLast) + { + dsqlScratch->appendUChar(blr_if); + + if (notMatched->condition) + notMatched->condition->genBlr(dsqlScratch); + else + { + dsqlScratch->appendUChar(blr_eql); + dsqlScratch->appendUChar(blr_literal); + dsqlScratch->appendUChar(blr_bool); + dsqlScratch->appendUChar(1); + dsqlScratch->appendUChar(blr_literal); + dsqlScratch->appendUChar(blr_bool); + dsqlScratch->appendUChar(1); + } + } + + if (notMatched->assignments) // UPDATE + { + dsqlScratch->appendUChar(returning ? blr_modify2 : blr_modify); + GEN_stuff_context(dsqlScratch, oldContext); + GEN_stuff_context(dsqlScratch, notMatched->modifyRelation->dsqlContext); + dsqlScratch->putBlrMarkers(MARK_MERGE); + + dsqlScratch->appendUChar(blr_begin); + + auto valueIt = notMatched->processedValues.begin(); + + for (auto& field : notMatched->processedFields) + { + dsqlScratch->appendUChar(blr_assignment); + (*valueIt++)->genBlr(dsqlScratch); + field->genBlr(dsqlScratch); + } + + dsqlScratch->appendUChar(blr_end); + + if (returning) + dsqlGenReturning(dsqlScratch, notMatched->processedReturning, tableNumber); + } + else // DELETE + { + if (returning) + { + dsqlScratch->appendUChar(blr_begin); + dsqlGenReturning(dsqlScratch, notMatched->processedReturning, tableNumber); + } + + dsqlScratch->appendUChar(blr_erase); + GEN_stuff_context(dsqlScratch, oldContext); + dsqlScratch->putBlrMarkers(MARK_MERGE); + + if (returning) + dsqlScratch->appendUChar(blr_end); + } + + if (notMatched->condition && isLast) + dsqlScratch->appendUChar(blr_end); + } + + if (whenNotMatchedByTarget.isEmpty() && whenMatched.isEmpty()) + dsqlScratch->appendUChar(blr_end); + } + + if (whenNotMatchedByTarget.hasData() || whenMatched.hasData()) + { + dsqlScratch->appendUChar(blr_if); + + if (whenNotMatchedByTarget.isEmpty()) + dsqlScratch->appendUChar(blr_not); + + dsqlScratch->appendUChar(blr_missing); + dsqlScratch->appendUChar(blr_dbkey); + GEN_stuff_context(dsqlScratch, oldContext); + } //// TODO: It should be possible, under certain circunstances, to use single blr_store for inserts //// and single blr_modify for updates. //// However, if there are inserts with different override clauses or deletes, this is not possible. - for (auto nextNotMatched = whenNotMatched.begin(), notMatched = nextNotMatched++; - notMatched != whenNotMatched.end(); + for (auto nextNotMatched = whenNotMatchedByTarget.begin(), notMatched = nextNotMatched++; + notMatched != whenNotMatchedByTarget.end(); notMatched = nextNotMatched++) { - const bool isLast = nextNotMatched == whenNotMatched.end(); + const bool isLast = nextNotMatched == whenNotMatchedByTarget.end(); if (notMatched->condition || !isLast) { @@ -5973,7 +6199,7 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (matched->assignments) // UPDATE { dsqlScratch->appendUChar(returning ? blr_modify2 : blr_modify); - GEN_stuff_context(dsqlScratch, targetContext); + GEN_stuff_context(dsqlScratch, oldContext); GEN_stuff_context(dsqlScratch, matched->modifyRelation->dsqlContext); dsqlScratch->putBlrMarkers(MARK_MERGE); @@ -6002,7 +6228,7 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) } dsqlScratch->appendUChar(blr_erase); - GEN_stuff_context(dsqlScratch, targetContext); + GEN_stuff_context(dsqlScratch, oldContext); dsqlScratch->putBlrMarkers(MARK_MERGE); if (returning) @@ -6013,8 +6239,11 @@ void MergeNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_end); } - if (whenNotMatched.hasData() != whenMatched.hasData()) + if ((whenNotMatchedByTarget.hasData() || whenMatched.hasData()) && + whenNotMatchedByTarget.hasData() != whenMatched.hasData()) + { dsqlScratch->appendUChar(blr_end); + } if (returning && !dsqlScratch->isPsql()) { @@ -6425,7 +6654,7 @@ void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (dsqlReturning && !dsqlScratch->isPsql()) { if (dsqlCursorName.isEmpty()) - dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturning, dsqlReturningLocalTableNumber.value); + dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturningLocalTableNumber.value); else { dsqlScratch->appendUChar(blr_send); @@ -7333,7 +7562,7 @@ void StoreNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (dsqlReturning && !dsqlScratch->isPsql()) { if (dsqlRse) - dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturning, dsqlReturningLocalTableNumber.value); + dsqlGenReturningLocalTableDecl(dsqlScratch, dsqlReturningLocalTableNumber.value); else if (!(dsqlScratch->flags & DsqlCompilerScratch::FLAG_UPDATE_OR_INSERT)) { dsqlScratch->appendUChar(blr_send); @@ -9388,17 +9617,17 @@ static void dsqlGenReturningLocalTableCursor(DsqlCompilerScratch* dsqlScratch, R } // Generate BLR for returning's local table declaration. -static void dsqlGenReturningLocalTableDecl(DsqlCompilerScratch* dsqlScratch, ReturningClause* returning, USHORT tableNumber) +static void dsqlGenReturningLocalTableDecl(DsqlCompilerScratch* dsqlScratch, USHORT tableNumber) { dsqlScratch->appendUChar(blr_dcl_local_table); dsqlScratch->appendUShort(tableNumber); dsqlScratch->appendUChar(blr_dcl_local_table_format); - dsqlScratch->appendUShort(returning->first->items.getCount()); + dsqlScratch->appendUShort(dsqlScratch->returningClause->second->items.getCount()); - for (auto& retSource : returning->first->items) + for (auto& retTarget : dsqlScratch->returningClause->second->items) { dsc fieldDesc; - DsqlDescMaker::fromNode(dsqlScratch, &fieldDesc, retSource); + DsqlDescMaker::fromNode(dsqlScratch, &fieldDesc, retTarget); GEN_descriptor(dsqlScratch, &fieldDesc, true); } diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index 31b257e72c..3397aaf97d 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -1147,7 +1147,9 @@ public: explicit MergeNode(MemoryPool& pool) : TypedNode(pool), whenMatched(pool), - whenNotMatched(pool) + whenNotMatchedByTarget(pool), + whenNotMatchedBySource(pool), + usingContexts(pool) { } @@ -1160,13 +1162,15 @@ public: NestConst usingClause; NestConst condition; Firebird::ObjectsArray whenMatched; - Firebird::ObjectsArray whenNotMatched; + Firebird::ObjectsArray whenNotMatchedByTarget; + Firebird::ObjectsArray whenNotMatchedBySource; NestConst plan; NestConst order; NestConst returning; NestConst rse; - dsql_ctx* targetContext = nullptr; + dsql_ctx* oldContext = nullptr; + Firebird::SortedArray usingContexts; }; diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 2ec1a73ce5..f27e30b5c7 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -684,6 +684,7 @@ using namespace Firebird; // tokens added for Firebird 5.0 +%token TARGET %token TIMEZONE_NAME %token UNICODE_CHAR %token UNICODE_VAL @@ -6586,9 +6587,17 @@ merge_when_matched_clause($mergeNode) %type merge_when_not_matched_clause() merge_when_not_matched_clause($mergeNode) - : WHEN NOT MATCHED - { $$ = &$mergeNode->whenNotMatched.add(); } - merge_insert_specification(NOTRIAL($4)) + : WHEN NOT MATCHED by_target_noise + { $$ = &$mergeNode->whenNotMatchedByTarget.add(); } + merge_insert_specification(NOTRIAL($5)) + | WHEN NOT MATCHED BY SOURCE + { $$ = &$mergeNode->whenNotMatchedBySource.add(); } + merge_update_specification(NOTRIAL($6), NOTRIAL(&$mergeNode->relation->dsqlName)) + ; + +by_target_noise + : // empty + | BY TARGET ; %type merge_update_specification(, ) @@ -9105,7 +9114,8 @@ non_reserved_word | ZONE | DEBUG // added in FB 4.0.1 | PKCS_1_5 - | TIMEZONE_NAME // added in FB 5.0 + | TARGET // added in FB 5.0 + | TIMEZONE_NAME | UNICODE_CHAR | UNICODE_VAL ; diff --git a/src/dsql/pass1.cpp b/src/dsql/pass1.cpp index 442c5a8802..9f98672126 100644 --- a/src/dsql/pass1.cpp +++ b/src/dsql/pass1.cpp @@ -178,7 +178,6 @@ using namespace Firebird; static string pass1_alias_concat(const string&, const string&); -static void pass1_expand_contexts(DsqlContextStack& contexts, dsql_ctx* context); static ValueListNode* pass1_group_by_list(DsqlCompilerScratch*, ValueListNode*, ValueListNode*); static ValueExprNode* pass1_make_derived_field(thread_db*, DsqlCompilerScratch*, ValueExprNode*); static RseNode* pass1_rse(DsqlCompilerScratch*, RecordSourceNode*, ValueListNode*, RowsClause*, bool, USHORT); @@ -955,8 +954,9 @@ DeclareCursorNode* PASS1_cursor_name(DsqlCompilerScratch* dsqlScratch, const Met // Extract relation and procedure context and expand derived child contexts. -static void pass1_expand_contexts(DsqlContextStack& contexts, dsql_ctx* context) +void PASS1_expand_contexts(DsqlContextStack& contexts, dsql_ctx* context) { + //// TODO: LocalTableSourceNode if (context->ctx_relation || context->ctx_procedure || context->ctx_map || context->ctx_win_maps.hasData()) { @@ -968,7 +968,7 @@ static void pass1_expand_contexts(DsqlContextStack& contexts, dsql_ctx* context) else { for (DsqlContextStack::iterator i(context->ctx_childs_derived_table); i.hasData(); ++i) - pass1_expand_contexts(contexts, i.object()); + PASS1_expand_contexts(contexts, i.object()); } } @@ -1089,7 +1089,7 @@ RseNode* PASS1_derived_table(DsqlCompilerScratch* dsqlScratch, SelectExprNode* i context->ctx_childs_derived_table.push(childCtx); // Collect contexts that will be used for blr_derived_expr generation. - pass1_expand_contexts(context->ctx_main_derived_contexts, childCtx); + PASS1_expand_contexts(context->ctx_main_derived_contexts, childCtx); } while (temp.hasData()) diff --git a/src/dsql/pass1_proto.h b/src/dsql/pass1_proto.h index 45de26b6d7..a9c1d2b8ea 100644 --- a/src/dsql/pass1_proto.h +++ b/src/dsql/pass1_proto.h @@ -42,6 +42,7 @@ void PASS1_check_unique_fields_names(Jrd::StrArray& names, const Jrd::CompoundSt Jrd::BoolExprNode* PASS1_compose(Jrd::BoolExprNode*, Jrd::BoolExprNode*, UCHAR); Jrd::DeclareCursorNode* PASS1_cursor_name(Jrd::DsqlCompilerScratch*, const Jrd::MetaName&, USHORT, bool); Jrd::RseNode* PASS1_derived_table(Jrd::DsqlCompilerScratch*, Jrd::SelectExprNode*, const char*, bool); +void PASS1_expand_contexts(Jrd::DsqlContextStack& contexts, Jrd::dsql_ctx* context); Jrd::ValueListNode* PASS1_expand_select_list(Jrd::DsqlCompilerScratch*, Jrd::ValueListNode*, Jrd::RecSourceListNode*); void PASS1_expand_select_node(Jrd::DsqlCompilerScratch*, Jrd::ExprNode*, Jrd::ValueListNode*, bool); void PASS1_field_unknown(const TEXT*, const TEXT*, const Jrd::ExprNode*);