What happens?
Planning the following query with EXPLAIN crashes with Error: unordered_map::at (a std::out_of_range escaping as an error, not a segfault). The same query without EXPLAIN executes fine, so the failure is in plan mapping on the EXPLAIN path.
The throw originates in Schema::getGroupPos(name) (unordered_map::at) reached via:
#2 lbug::planner::Schema::getGroupPos(std::string const&) const
#3 lbug::planner::Schema::getGroupPos(lbug::binder::Expression const&) const
#4 lbug::planner::Schema::getExpressionPos(lbug::binder::Expression const&) const
#5 lbug::processor::PlanMapper::mapAggregate(lbug::planner::LogicalOperator const*)
i.e. tryMapPackedFilteredCount() (src/processor/map/map_aggregate.cpp) calls packedChildSchema->getExpressionPos(...) on the group key and on the two predicate inputs of the (lhs + rhs) % k = 0 filter. When a predicate input is a plain property (b.id) the lookup succeeds; when it is a computed expression over the nbr internal ID (offset(id(b))) the expression is not found in the packed child schema by unique name and .at() throws.
Repro
-- any database with a two-column int64 PK node table and a rel table; e.g.:
CREATE NODE TABLE MPerson(id INT64, PRIMARY KEY(id));
CREATE REL TABLE MKnows(FROM MPerson TO MPerson);
-- (populate some edges)
CALL enable_packed_path_extend=true;
EXPLAIN MATCH (a:MPerson)-[e:MKnows]->(b:MPerson)
WHERE (a.id + offset(id(b))) % 10 = 0
RETURN a.id, COUNT(*);
Observed on the current main build (pre-existing; found while testing #867, which only touches this code path's gating and does not change the crash).
Conditions (verified on a 3000-node / 6750-edge MPerson/MKnows database)
| # |
config |
query shape |
result |
| A |
enable_packed_path_extend=true + EXPLAIN |
keyed COUNT(*), predicate (a.id + offset(id(b))) % 10 = 0 |
crash (unordered_map::at) |
| B |
enable_packed_path_extend=false + EXPLAIN |
same |
OK |
| C |
enable_packed_path_extend=true + EXPLAIN |
same, but predicate (a.id + b.id) % 10 = 0 (plain property) |
OK |
| D |
enable_packed_path_extend=true + EXPLAIN |
global COUNT(*) (no group key), offset predicate |
OK |
| — |
enable_packed_path_extend=true, no EXPLAIN |
same as A |
OK (executes and returns correct rows) |
So all three ingredients are required: the packed-filtered-count mapping pattern (keyed count(*) over a single modulo-sum filter above a packed extend), a predicate input that is a computed expression rather than a property, and the EXPLAIN statement (non-EXPLAIN execution of the same query does not hit the throwing lookup).
Expected
Either the plan maps (the schema should contain, or the mapper should resolve, the mapped form of the predicate input expressions), or tryMapPackedFilteredCount should bail out gracefully (return nullptr) when the key/predicate-input expressions cannot be located in packedChildSchema, falling back to the regular aggregate plan.
Suggested fix direction
In tryMapPackedFilteredCount, the lookups packedChildSchema->getExpressionPos(*key) / getExpressionPos(*predicateInputs->first|second) assume the binder expressions appear verbatim in the packed child schema. For computed expressions (e.g. offset(id(b))) the planned schema holds a different expression object for the same unique name, or holds nothing at all. Guard these lookups (e.g. Schema::containsExpression(...)-style check, or try/catch around getGroupPos) and fall back to the standard aggregate mapping when they fail. It may also be worth checking why the EXPLAIN path differs from direct execution for the same query.
What happens?
Planning the following query with
EXPLAINcrashes withError: unordered_map::at(astd::out_of_rangeescaping as an error, not a segfault). The same query withoutEXPLAINexecutes fine, so the failure is in plan mapping on the EXPLAIN path.The throw originates in
Schema::getGroupPos(name)(unordered_map::at) reached via:i.e.
tryMapPackedFilteredCount()(src/processor/map/map_aggregate.cpp) callspackedChildSchema->getExpressionPos(...)on the group key and on the two predicate inputs of the(lhs + rhs) % k = 0filter. When a predicate input is a plain property (b.id) the lookup succeeds; when it is a computed expression over the nbr internal ID (offset(id(b))) the expression is not found in the packed child schema by unique name and.at()throws.Repro
Observed on the current
mainbuild (pre-existing; found while testing #867, which only touches this code path's gating and does not change the crash).Conditions (verified on a 3000-node / 6750-edge
MPerson/MKnowsdatabase)enable_packed_path_extend=true+EXPLAINCOUNT(*), predicate(a.id + offset(id(b))) % 10 = 0unordered_map::at)enable_packed_path_extend=false+EXPLAINenable_packed_path_extend=true+EXPLAIN(a.id + b.id) % 10 = 0(plain property)enable_packed_path_extend=true+EXPLAINCOUNT(*)(no group key), offset predicateenable_packed_path_extend=true, noEXPLAINSo all three ingredients are required: the packed-filtered-count mapping pattern (keyed
count(*)over a single modulo-sum filter above a packed extend), a predicate input that is a computed expression rather than a property, and theEXPLAINstatement (non-EXPLAIN execution of the same query does not hit the throwing lookup).Expected
Either the plan maps (the schema should contain, or the mapper should resolve, the mapped form of the predicate input expressions), or
tryMapPackedFilteredCountshould bail out gracefully (returnnullptr) when the key/predicate-input expressions cannot be located inpackedChildSchema, falling back to the regular aggregate plan.Suggested fix direction
In
tryMapPackedFilteredCount, the lookupspackedChildSchema->getExpressionPos(*key)/getExpressionPos(*predicateInputs->first|second)assume the binder expressions appear verbatim in the packed child schema. For computed expressions (e.g.offset(id(b))) the planned schema holds a different expression object for the same unique name, or holds nothing at all. Guard these lookups (e.g.Schema::containsExpression(...)-style check, or try/catch aroundgetGroupPos) and fall back to the standard aggregate mapping when they fail. It may also be worth checking why the EXPLAIN path differs from direct execution for the same query.