Compare commits

...

5 Commits

Author SHA1 Message Date
Josipmrden
e4327de5c6
Merge branch 'master' into forbid-merge-with-null-properties 2024-03-13 12:09:57 +01:00
Josip Mrden
9e73ddeabe Add proper logic of handling merge runtime 2024-03-13 12:07:32 +01:00
Josip Mrden
9ef02772cc Merge branch 'master' into forbid-merge-with-null-properties 2024-03-13 00:50:04 +01:00
Josip Mrden
0b762d37ba Add new implementation for runtime checking of merge 2024-03-13 00:45:22 +01:00
Josip Mrden
bfd8921777 Forbid merge with null properties 2024-03-12 12:37:43 +01:00
4 changed files with 211 additions and 6 deletions

View File

@ -35,6 +35,10 @@ enum class TransactionStatus {
STARTED_ROLLBACK, STARTED_ROLLBACK,
}; };
struct Scope {
bool in_merge{false};
};
struct EvaluationContext { struct EvaluationContext {
/// Memory for allocations during evaluation of a *single* Pull call. /// Memory for allocations during evaluation of a *single* Pull call.
/// ///
@ -51,6 +55,7 @@ struct EvaluationContext {
/// All counters generated by `counter` function, mutable because the function /// All counters generated by `counter` function, mutable because the function
/// modifies the values /// modifies the values
mutable std::unordered_map<std::string, int64_t> counters{}; mutable std::unordered_map<std::string, int64_t> counters{};
Scope scope{};
}; };
inline std::vector<storage::PropertyId> NamesToProperties(const std::vector<std::string> &property_names, inline std::vector<storage::PropertyId> NamesToProperties(const std::vector<std::string> &property_names,

View File

@ -263,6 +263,15 @@ VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *fram
properties.emplace(dba.NameToProperty(key), value); properties.emplace(dba.NameToProperty(key), value);
} }
} }
if (context.evaluation_context.scope.in_merge) {
for (const auto &[k, v] : properties) {
if (v.IsNull()) {
throw QueryRuntimeException(fmt::format("Can't have null literal properties inside merge ({}.{})!",
node_info.symbol.name(), dba.PropertyToName(k)));
}
}
}
MultiPropsInitChecked(&new_node, properties); MultiPropsInitChecked(&new_node, properties);
(*frame)[node_info.symbol] = new_node; (*frame)[node_info.symbol] = new_node;
@ -346,7 +355,7 @@ CreateExpand::CreateExpandCursor::CreateExpandCursor(const CreateExpand &self, u
namespace { namespace {
EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, VertexAccessor *from, VertexAccessor *to, EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, VertexAccessor *from, VertexAccessor *to,
Frame *frame, ExpressionEvaluator *evaluator) { Frame *frame, ExecutionContext &context, ExpressionEvaluator *evaluator) {
auto maybe_edge = dba->InsertEdge(from, to, edge_info.edge_type); auto maybe_edge = dba->InsertEdge(from, to, edge_info.edge_type);
if (maybe_edge.HasValue()) { if (maybe_edge.HasValue()) {
auto &edge = *maybe_edge; auto &edge = *maybe_edge;
@ -361,6 +370,14 @@ EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, Vert
properties.emplace(dba->NameToProperty(key), value); properties.emplace(dba->NameToProperty(key), value);
} }
} }
if (context.evaluation_context.scope.in_merge) {
for (const auto &[k, v] : properties) {
if (v.IsNull()) {
throw QueryRuntimeException(fmt::format("Can't have null literal properties inside merge ({}.{})!",
edge_info.symbol.name(), dba->PropertyToName(k)));
}
}
}
if (!properties.empty()) MultiPropsInitChecked(&edge, properties); if (!properties.empty()) MultiPropsInitChecked(&edge, properties);
(*frame)[edge_info.symbol] = edge; (*frame)[edge_info.symbol] = edge;
@ -420,14 +437,14 @@ bool CreateExpand::CreateExpandCursor::Pull(Frame &frame, ExecutionContext &cont
auto created_edge = [&] { auto created_edge = [&] {
switch (self_.edge_info_.direction) { switch (self_.edge_info_.direction) {
case EdgeAtom::Direction::IN: case EdgeAtom::Direction::IN:
return CreateEdge(self_.edge_info_, dba, &v2, &v1, &frame, &evaluator); return CreateEdge(self_.edge_info_, dba, &v2, &v1, &frame, context, &evaluator);
case EdgeAtom::Direction::OUT: case EdgeAtom::Direction::OUT:
// in the case of an undirected CreateExpand we choose an arbitrary // in the case of an undirected CreateExpand we choose an arbitrary
// direction. this is used in the MERGE clause // direction. this is used in the MERGE clause
// it is not allowed in the CREATE clause, and the semantic // it is not allowed in the CREATE clause, and the semantic
// checker needs to ensure it doesn't reach this point // checker needs to ensure it doesn't reach this point
case EdgeAtom::Direction::BOTH: case EdgeAtom::Direction::BOTH:
return CreateEdge(self_.edge_info_, dba, &v1, &v2, &frame, &evaluator); return CreateEdge(self_.edge_info_, dba, &v1, &v2, &frame, context, &evaluator);
} }
}(); }();
@ -4317,6 +4334,9 @@ bool Merge::MergeCursor::Pull(Frame &frame, ExecutionContext &context) {
OOMExceptionEnabler oom_exception; OOMExceptionEnabler oom_exception;
SCOPED_PROFILE_OP("Merge"); SCOPED_PROFILE_OP("Merge");
context.evaluation_context.scope.in_merge = true;
memgraph::utils::OnScopeExit merge_exit([&] { context.evaluation_context.scope.in_merge = false; });
while (true) { while (true) {
if (pull_input_) { if (pull_input_) {
if (input_cursor_->Pull(frame, context)) { if (input_cursor_->Pull(frame, context)) {
@ -4325,9 +4345,10 @@ bool Merge::MergeCursor::Pull(Frame &frame, ExecutionContext &context) {
// and merge_create (could have a Once at the beginning) // and merge_create (could have a Once at the beginning)
merge_match_cursor_->Reset(); merge_match_cursor_->Reset();
merge_create_cursor_->Reset(); merge_create_cursor_->Reset();
} else } else {
// input is exhausted, we're done // input is exhausted, we're done
return false; return false;
}
} }
// pull from the merge_match cursor // pull from the merge_match cursor

View File

@ -187,7 +187,7 @@ Feature: Merge feature
Then the result should be: Then the result should be:
| r | | r |
| [:X] | | [:X] |
Scenario: Merge relationship test02 Scenario: Merge relationship test02
Given an empty graph Given an empty graph
And having executed: And having executed:
@ -417,4 +417,93 @@ Feature: Merge feature
| ({a: 1}) | | ({a: 1}) |
| ({a: 2}) | | ({a: 2}) |
| ({a: 3)) | | ({a: 3)) |
Scenario: Merge node with null property error
Given an empty graph
When executing query:
"""
MERGE ({id: null})
"""
Then an error should be raised
Scenario: Merge node with one null property error
Given an empty graph
When executing query:
"""
MERGE ({id2: 1, id: null})
"""
Then an error should be raised
Scenario: Merge edge with null property error
Given an empty graph
When executing query:
"""
MERGE ()-[:TYPE {id:null}]->()
"""
Then an error should be raised
Scenario: Merge node with unwind with null property error
Given an empty graph
When executing query:
"""
UNWIND [1, 2, null, 3] as x
MERGE ({id: x})
"""
Then an error should be raised
Scenario: Merge edge with unwind with null property error
Given an empty graph
When executing query:
"""
UNWIND [1, 2, null, 3] as x
MERGE ()-[:TYPE {id:x}]->()
"""
Then an error should be raised
Scenario: Merge node with null in on match passes
Given an empty graph
And having executed:
"""
CREATE ({a: 1})
"""
When executing query:
"""
MERGE (n {a: 1})
ON MATCH SET n.prop = null
ON CREATE SET n.prop = null;
"""
Then the result should be empty
Scenario: Merge edge with null in on match passes
Given an empty graph
And having executed:
"""
CREATE ()-[:TYPE {a: 1}]->()
"""
When executing query:
"""
MERGE ()-[r:TYPE {a: 1}]->()
ON MATCH SET r.prop = null
ON CREATE SET r.prop = null;
"""
Then the result should be empty
Scenario: Merge node with null in on create passes
Given an empty graph
When executing query:
"""
MERGE (n {a: 1})
ON MATCH SET n.prop = null
ON CREATE SET n.prop = null;
"""
Then the result should be empty
Scenario: Merge edge with null in on create passes
Given an empty graph
When executing query:
"""
MERGE ()-[r:TYPE {a: 1}]->()
ON MATCH SET r.prop = null
ON CREATE SET r.prop = null;
"""
Then the result should be empty

View File

@ -417,3 +417,93 @@ Feature: Merge feature
| ({a: 1}) | | ({a: 1}) |
| ({a: 2}) | | ({a: 2}) |
| ({a: 3)) | | ({a: 3)) |
Scenario: Merge node with null property error
Given an empty graph
When executing query:
"""
MERGE ({id: null})
"""
Then an error should be raised
Scenario: Merge node with one null property error
Given an empty graph
When executing query:
"""
MERGE ({id2: 1, id: null})
"""
Then an error should be raised
Scenario: Merge edge with null property error
Given an empty graph
When executing query:
"""
MERGE ()-[:TYPE {id:null}]->()
"""
Then an error should be raised
Scenario: Merge node with unwind with null property error
Given an empty graph
When executing query:
"""
UNWIND [1, 2, null, 3] as x
MERGE ({id: x})
"""
Then an error should be raised
Scenario: Merge edge with unwind with null property error
Given an empty graph
When executing query:
"""
UNWIND [1, 2, null, 3] as x
MERGE ()-[:TYPE {id:x}]->()
"""
Then an error should be raised
Scenario: Merge node with null in on match passes
Given an empty graph
And having executed:
"""
CREATE ({a: 1})
"""
When executing query:
"""
MERGE (n {a: 1})
ON MATCH SET n.prop = null
ON CREATE SET n.prop = null;
"""
Then the result should be empty
Scenario: Merge edge with null in on match passes
Given an empty graph
And having executed:
"""
CREATE ()-[:TYPE {a: 1}]->()
"""
When executing query:
"""
MERGE ()-[r:TYPE {a: 1}]->()
ON MATCH SET r.prop = null
ON CREATE SET r.prop = null;
"""
Then the result should be empty
Scenario: Merge node with null in on create passes
Given an empty graph
When executing query:
"""
MERGE (n {a: 1})
ON MATCH SET n.prop = null
ON CREATE SET n.prop = null;
"""
Then the result should be empty
Scenario: Merge edge with null in on create passes
Given an empty graph
When executing query:
"""
MERGE ()-[r:TYPE {a: 1}]->()
ON MATCH SET r.prop = null
ON CREATE SET r.prop = null;
"""
Then the result should be empty