{{ message }}
[experiment] enhanced projection#4292
Draft
Ignition wants to merge 73 commits into
Draft
Conversation
Confirm the read/consume path of a project() subgraph end to end: an algorithm-shaped procedure that iterates the subgraph's vertices, groups them by a property, and yields (node, value) sees the real property values, and the yielded nodes are real accessors whose ids resolve to the real store. The existing project() coverage yields nodes and edges but never asserts that a property read inside the procedure returns the real value or that a yielded node's identity is the real one. This closes that gap, and the id assertion distinguishes a real accessor from a synthetic copy (which would carry a synthetic id counted down from the top of the id space).
Capture five ready-for-human tasks that need design or profiling judgement before they can be handed to an agent: virtual/overlay memory-layout profiling, a review of Neo4j GDS projection prior art, a concise declarative projection surface, extending functions over non-native nodes, and unifying the three node kinds to cut special-case branching.
Capture the Cypher-execution-and-write-back-on-projections feature: the PRD plus the eleven issues it decomposes into, covering the project()/derive() constructors, virtual node/edge constructors, per-property binding, overlay write-back, Bolt marshalling, and the ready-for-human Cypher-on-projection design.
Confirm the MVP write-back path: a SET on a node yielded by an algorithm run over a project() subgraph persists to the real store. An algorithm-shaped procedure yields (node, value) over the subgraph, the outer query writes the value onto each yielded node, and a follow-up statement reads the written values back from the real nodes. The property written is previously absent, so this also covers a write creating a new property on the real node, not just overwriting an existing one.
Add virtualNode(handle, labels, properties), a query function that constructs a synthetic node: a node with no origin, holding an overlay property store only. Labels may be a single string or a list of strings; properties is a map. The node takes a fresh synthetic gid as its identity, so it never collides with a real node, and renders over Bolt as an ordinary node. The first argument is a user-facing logical handle, kept distinct from the node's identity so that a virtual edge can later reference an endpoint by handle when a projection is assembled. The handle is accepted and validated here; persisting it on the node lands with virtualEdge, its first consumer. The function is not pure: each call mints a new identity, so its result must not be cached. Wrong arity, a null or non-string label, and a non-integer handle are rejected at evaluation with clear messages.
A SET targeting a synthetic node now writes its overlay store instead of
raising 'Properties can only be set on edges and vertices'. All three
property forms are supported: SET n.key = value (and = null to remove the
key), SET n += {..} to merge a map, and SET n = {..} to replace the overlay.
Label writes are not yet handled.
A synthetic node has value semantics in a result value, unlike a real node
which is a storage handle, so the write targets the frame-resident node in
place rather than an evaluated copy; the per-property form therefore requires
the node to be named. The right-hand side of a map SET must be a map, since a
synthetic node has no record to copy properties from.
Slices 01 (project subgraph) and 02 (write-back) were already satisfied on master and are confirmed by e2e tests; 03 (virtualNode) is implemented. Record the decision that a synthetic node's identity is its own synthetic gid and the virtualNode() gid argument is a logical handle, with handle storage deferred to 04 where virtual edges first consume it.
Record the decision to model every node in a derived view with one value type holding an optional origin vertex plus an overlay store: a synthetic node is the no-origin case, an overlay node the origin case. This collapses the per-node-kind branching in functions, SET, and Bolt into one projected-node path, and turns the overlay read-through, per-property binding, write-back, and provenance slices into method bodies rather than new node kinds. Records the settled choices on naming, the VertexAccessor-shaped read signature, and edges referencing nodes (the gid form resolved at list assembly).
A node from derive() now keeps a reference to its origin real vertex instead of copying its properties. Property reads fall through to the origin at read time (latest transaction view, never cached), and an explicitly-overridden property shadows the origin. Constructing a projection therefore no longer duplicates the origin's properties (e.g. vector embeddings) unless they are actually read. VirtualNode gains an optional origin held in its heap allocation, so its inline size and the mgp object budgets that embed it are unchanged. A node with no origin (virtualNode(), list import) is unaffected: its overlay is its only store. Properties() returns the merged view by value, since the merge is not stored. The origin write path (per-property binding and write-back) is a separate slice; a SET on an overlay node still writes the overlay.
A derive() projection now takes a propertyPolicy map binding each property to 'origin' (read-through), 'overlay' (an overlay value shadows the origin), or 'hidden'. A hidden property is invisible to reads and to function calls over the node: GetProperty returns null and Properties() omits it. Unlisted properties read through to the origin as before. Read source and write target are coupled to one store per property, so binding a key to 'origin' while also overlaying it (via sourceNodeProperties) is rejected at construction with a clear message rather than resolved silently. The hidden key set is fixed at construction and lives in the node's heap allocation, so the node's inline size and the mgp object budgets are unchanged.
A SET through a derive() overlay node now follows the node's static per-property binding instead of always landing in the overlay. An overlay-bound key (declared 'overlay' in propertyPolicy, or given a construction-time override value) writes the overlay and is never persisted, so Memgraph can serve as a compute layer. An origin-bound or undeclared key persists to the real origin vertex, completing the headline path: run an algorithm over a projection and write the score back onto the underlying node. Read source and write target are coupled to one store per key, so a single predicate decides both. An origin write also clears any overlay entry for that key, so a read-after-write returns the persisted value rather than a stale shadow. A synthetic node (no origin) is unchanged: every write mutates its overlay and never errors. e2e tests cover the three write targets, read-after-write consistency, and algorithm write-back persisting scores through a procedure.
An overlay node from derive() now serializes over Bolt at its origin's id rather than its synthetic gid, so a click, expand, or edit in a client resolves to the real underlying node. The property map is the merged view already produced by the node (overlay values shadow the origin). A synthetic node, having no origin, still serializes with its synthetic id. This touches only which id and which properties are written; the Bolt Node structure shape is unchanged, so existing drivers decode the result without modification. e2e tests assert the origin id and merged map for an overlay node, the overlay value shadowing in the serialized map, and the synthetic id for a node with no origin.
An overlay node produced by a derive() now carries a reserved Int property, __mg_overlay_ref, identifying which projection it came from. The value is the derive() output symbol's plan position: it is stable, unique per derive() site, and shared by every overlay node from that site, so all nodes of one projection reference a single schema entry. Synthetic nodes and real nodes carry no tag. This is the per-node half of the Bolt provenance channel. A projection-aware client (Lab) reads the tag to find the node's schema; a generic client sees an ordinary Int property and ignores it. The value is sent unwrapped, so existing clients decode the result unchanged. The schema table the tag points into is delivered separately in the RUN header. The reference rides in VirtualNode's heap-allocated Impl, defaulting to a sentinel that means "no projection", so the node stays small for the mgp_vertex/mgp_edge budgets that embed it.
A derive() projection stamps its overlay nodes with a schema reference only when its options are a static map literal. When the options are not statically known (for example the whole map is a query parameter), the projection's schema cannot be determined before execution, so the overlay nodes now carry no reference. This keeps the per-node reference and the schema description it points at in agreement: a node is tagged only when there will be a matching schema entry to resolve it against. The nodes still serialize at their origin identity either way; only the styling metadata is absent for a non-literal projection. No error is raised.
A query that builds derive() projections now sends a projection-schema table once in the RUN response header, alongside the result fields. The table is keyed by each projection's reference - the same Int every overlay node from that derive() carries in its reserved tag property - so a projection-aware client (Lab) resolves a node's provenance as it arrives, without buffering the result. Each entry lists the overlay-bound property keys (the computed ones, distinct from the origin's real properties) and the virtual edge type, in an extensible map so more styling metadata can be added without a wire change. The table is built statically from the plan at prepare time, before any record streams. For each derive() whose options are a literal map, the overlay key set and edge type are read from the options - the map structure survives literal stripping, and the stripped scalar values are recovered by evaluating them against the query parameters, matching the bindings the executor will apply. A derive() whose options are not a literal map produces no entry, agreeing with the executor leaving those nodes untagged. Entries flow from the plan through prepare into the Bolt RUN metadata under projection_schema. The key is unknown to existing drivers and ignored; the entry values are plain maps, lists, and strings that a generic decoder reads with no special handling. Omitted entirely for queries with no projection.
The per-node tag and the RUN-header schema table are both implemented and tested. Records the locked-design departure on the wire: hidden keys are not listed in a schema entry, since a hidden key is already absent from the serialized property map and its name alone carries no styling value.
…ints
A synthetic edge built by virtualEdge() may name an endpoint by a virtual
node or by a user gid. ADR 0001 held that the gid is resolved only at
projection assembly and stored neither on the node nor on the edge. Eager
evaluation rules that out: virtualEdge("T", 1, 2) becomes a value before any
assembly runs, and a node carries no declared gid to match against, so both
the handle and the edge's unresolved endpoints must be stored.
Record the handle as an import key stored on the node, distinct from its
synthetic identity and never serialized, and a synthetic-edge endpoint as
either a resolved node or an unresolved handle bound at assembly. ADR 0002
supersedes the edge-and-handle decisions of ADR 0001; CONTEXT.md gains the
synthetic-edge and unresolved-endpoint terms.
virtualNode(handle, ...) now keeps its handle argument on the node as an optional field, distinct from the node's synthetic identity and never serialized. A node built by derive() carries none. The handle is the key a later projection assembly uses to wire virtual edges to this node by reference; storing it here is what lets that assembly match endpoints.
A virtual edge endpoint is now either a resolved virtual node or an
unresolved import handle, settled per endpoint so the two may be mixed. This
is what lets virtualEdge("T", 1, 2) exist as a value before any assembly runs:
the handle rides on the edge until a projection assembly binds it to a node.
FromGid/ToGid return the node's synthetic gid when resolved and the handle
itself when unresolved, so Bolt serialization, the in/out index, and edge
equality work unchanged for both forms. Reaching for an endpoint node on an
unresolved edge (startNode/endNode) raises a clear query error. The endpoints
move into the heap-allocated Impl, keeping the edge within the mgp_edge size
budget.
virtualEdge(type, from, to) builds a synthetic edge. Each endpoint is a virtual node (a resolved endpoint) or a gid handle (an unresolved endpoint bound to a node when a projection is assembled from lists), and the two forms may be mixed. A real vertex is rejected with a message directing the user to pass its id() as the handle. The edge serializes over Bolt at its own synthetic id, with its endpoints' gids as the relationship's start and end. e2e tests cover both endpoint forms, the mixed form, the real-vertex rejection, and that startNode/endNode on an unresolved edge errors.
Settle the surface for assembling a projection from lists of synthetic elements: a scalar virtualGraph(nodes, edges, config) constructor, not an aggregation. Modelling it as a function avoids the aggregation framework's plan-time accumulator type and two-expression AST limit, leaves the shipped real project()/derive() aggregations untouched, and completes the virtualNode /virtualEdge family. Record the handle/synthetic-gid binding model, the dangling-edge term and its error/drop config, and that duplicate import handles are always an error. ADR 0003 captures the decision and alternatives; CONTEXT.md gains the dangling-edge term and notes virtualGraph as a projection producer.
AssembleVirtualGraph builds a projection from lists of synthetic nodes and edges. Nodes are inserted and indexed by their import handle; an edge binds to a listed node per endpoint - an unresolved handle endpoint by matching handle, a resolved endpoint by synthetic-gid membership - then is inserted. An edge endpoint that matches no listed node is a dangling edge: it aborts the assembly under kError or is omitted under kDrop. A duplicate import handle among the nodes makes a handle reference ambiguous and is always an error, independent of the dangling policy.
virtualGraph(nodes, edges, config?) assembles a projection from a list of synthetic nodes and a list of synthetic edges, returning a VirtualGraph an algorithm procedure can read. Nulls in either list are skipped; a real vertex or edge is rejected with a message pointing at project()/derive(). The optional config map's onDanglingEdge key selects 'error' (the default) or 'drop' for an edge whose endpoint matches no listed node. e2e tests cover import consumed by a procedure, the error default, drop mode, null skipping, and real-element rejection.
USE parses and is enforced read-only. Correct the issue-17 design grammar to
place the use-clause inside the block (CALL { USE g ... }), matching the ADR
example and the implementation, rather than between CALL and the brace.
Widen the scan element to the common form a GraphView yields: a real VertexAccessor or a projection's VirtualNode. VertexRange now backs either the real accessor's vertices or a VirtualGraph's nodes, and ScanAll writes whichever to the frame, which already carries both as a TypedValue. Fine-grained auth filters real vertices and lets projection nodes through, since they carry no real-graph privileges. VirtualGraphView becomes the projection GraphView: a scan over it yields the projection's nodes, so the read operators run over a projection the same way they run over the real graph. The real-graph scan is unchanged - it yields the VertexAccessor arm - and the boundary stays per-scan, not per-row. A unit test scans a synthetic projection through a bound view and gets its VirtualNodes on the frame from the same ScanAll that scans the real graph.
Add the BindGraphView operator: it wraps a CALL subquery's plan, and while
pulling the body it evaluates the bound graph expression to a projection value
and sets it as the execution context's ambient GraphView, restoring the
previous view around each pull. The rule-based planner emits it at the head of
the subquery plan when the subquery carries a USE clause.
With this, `WITH virtualGraph(...) AS g CALL { USE g MATCH (n) RETURN n.x }`
scans the projection's nodes inside the block while a MATCH outside the block
still reads the real graph. The body is planned full-scan: the index-lookup,
edge-index and join rewriters treat it as an isolated branch, since a projection
exposes no index.
End-to-end tests assert both the returned projection rows and that the scope is
bound to the projection inside the block and the real graph outside it.
Add e2e query tests for `CALL { USE g ... }`: the scope scans the bound
projection's nodes, the binding is scoped to the block (a MATCH outside sees the
real graph), and a write clause inside the scope is rejected. Mirrors the
in-process interpreter tests over the Bolt query path.
The first end-to-end USE scan landed: ScanAll runs over a projection through the
GraphView seam, and BindGraphView binds the projection as the ambient view for a
CALL { USE ... } block. Record how it landed and the verifying tests.
Make MATCH (a)-[r]->(b) inside a `CALL { USE g ... }` scope traverse the bound
projection's edges. The Expand cursor now dispatches on the input node it pulls:
a real VertexAccessor takes the existing real-graph path, while a projection's
VirtualNode reaches its incident edges through the projection view bound in the
execution context. Each emitted edge and its other endpoint are written to the
frame as the virtual TypedValue variants, so the endpoints bind back to
projection nodes and downstream operators read them unchanged.
The expansion covers directed and undirected patterns, an edge-type filter, and
self-loops, which appear when expanding either direction and collapse to a
single row when expanded both ways. The real-graph expand path is untouched.
Interpreter and e2e tests cover each case over a synthetic projection assembled
from node and edge lists.
…scope
Make MATCH (n:Label) and a WHERE property predicate work inside a
`CALL { USE g ... }` scope. A projection node carries its labels as names and
exposes no index, so the label test now evaluates over a VirtualNode by matching
the pattern's labels against the node's label list; the property predicate
already evaluated over a projection node and is unchanged.
Keep the scan a full scan. The index, edge-index, and join plan rewriters no
longer descend into a USE body: an index or join scan reaches the graph through
the real accessor, not the bound projection, so substituting one inside the
scope would read the real graph instead. A labelled or property-filtered match
inside the scope stays a ScanAll followed by a Filter. As a result a
MATCH (n:Label) inside the scope returns the projection's labelled nodes even
when the real graph carries an index on that label, while the same match outside
the scope still uses the real index.
Record the full-scan-only regime and the rewriter boundary in the projection
scope ADR. Interpreter and e2e tests cover label, multi-label, real-index
isolation, and property-predicate filtering over a synthetic projection.
…d signature Give VirtualNode the VertexAccessor-shaped property read: GetProperty(view, key) returning a storage::Result, and Properties(view) returning a Result. The origin read-through forwards the view straight through and propagates the origin's Result; an overlay-only read cannot fail but returns a Result for one uniform shape. The bare GetProperty(key) / Properties() forms stay for callers without a view (the procedure and function paths) and delegate to the view-aware ones, so hidden-key invisibility, overlay-key shadowing, and lazy origin read-through are unchanged. With the matching signature, the property read call site stops branching on node kind: PropertyLookup and a `[]` subscript read both a real vertex and a projected node through the same GetProperty helper. Indexing a projected node with `[]` now reads its property instead of being rejected, matching dotted access. Because the unified read forwards the evaluator's view rather than a fixed latest view, a projected node's origin reads behave like a real vertex's: a read after an origin-bound write reflects the persisted value.
A derive() projection is a VirtualGraph, so a USE scope binds it the same way it binds a virtualGraph() and reads its overlay nodes through the unified element read path. This composition needs no new code; lock it in with tests. Inside a USE scope over derive(), a MATCH reads through to the origin, a predicate over a read-through value filters correctly, an overlay-bound key shadows the origin, a hidden key is invisible to reads and to predicates, and an expand over the derived edge binds endpoints that read through to their origins. Interpreter tests and an e2e test class over a derive() overlay projection.
Bind a project() subgraph as a graph view and MATCH over it inside a USE scope, through the same GraphView seam as a projection - no parallel operator path. Add SubgraphGraphView, a view layered over the real graph by membership: a scan yields the subgraph's member vertices, and it answers whether an edge is a member. BindGraphView now binds either a VirtualGraph projection or a project() Graph value, constructing the matching view and setting it as the ambient view for the block. Expansion stays within the subgraph: while a real member of a bound subgraph is being expanded, Expand drops a real edge that is not a member, so a member node's edge to a non-member is not traversed. Outside a subgraph scope this filter is inert. Scanning runs unchanged through the ambient view, returning the member nodes as real vertices that read like any real vertex. Interpreter and e2e tests cover scanning the members and membership-respecting expansion over a project() subgraph.
Make degree, inDegree, and outDegree count edges in the graph bound for the
current scope, so inside a `CALL { USE ... }` block they report the projection's
or subgraph's topology, not the real graph's.
The ambient view moves from ExecutionContext onto EvaluationContext, the context
the expression evaluator already holds, and is threaded into FunctionContext.
The read operators and the USE-scope binding follow the field to its new home;
its default binding to the real graph is unchanged.
A degree over a projection node counts the projection's incident edges; a degree
over a subgraph member counts only the member edges, dropping a real edge to a
non-member; a real vertex with the identity view bound keeps its real-graph
degree. Neighbour and relationship traversal already runs over the bound view
through MATCH expansion, so no separate function path is needed.
Interpreter and e2e tests assert the in-scope degree differs from the same
node's real-graph degree, over both a derive() projection and a project()
subgraph.
…signature Complete the element read seam for edges. VirtualEdge gains the EdgeAccessor- shaped GetProperty(view, key) returning a storage::Result; a virtual edge has no origin to read through, so the view is unused and the read cannot fail, but the Result gives one uniform shape. The bare GetProperty(key) stays for callers without a view. With the matching signature, the property read call site stops branching on edge kind: PropertyLookup and a `[]` subscript read both a real edge and a projected edge through the same GetProperty helper, mirroring the node path. Indexing a projected edge with `[]` now reads its property instead of being rejected. This is the last collapsible read-path fork. The branch-point inventory gains a status section recording what the GraphView/USE arc retired - the read-path and topology-function node-kind dispatch - and what legitimately remains: the value functions over a standalone virtual value, which have no ambient accessor to resolve through.
plan_v2 has no awareness of the ambient graph view that a `CALL { USE g
... }` scope rebinds. Until now the lowering inspected the importing and
star scope clauses but ignored `use_graph_`, so with the experiment on a
non-importing `CALL { USE g RETURN ... }` lowered cleanly, the binding
was dropped, and the body scanned the real graph instead of the
projection: silently wrong results.
Guard `LowerCallSubquery` on `use_graph_` and throw NotYetImplemented
before any inner lowering, so the unsupported case surfaces loudly
rather than mishandled. plan_v2 USE support remains out of scope; this
only makes the gap fail fast. The default v1 planner still runs USE over
a projection unchanged.
The lowering test binds the graph to a literal so the only construct
plan_v2 cannot lower is USE itself, and a no-USE positive control of the
same subquery shape pins the throw to USE rather than to the CALL block.
Inside a `CALL { USE g ... }` scope a procedure that reads `ctx.graph`
operated on the real graph: the CallProcedure cursor built the
procedure's `mgp_graph` from the raw DbAccessor and never consulted the
ambient view the scope had bound, so an unmodified read procedure
silently read the real graph instead of the projection.
Extend the procedure call's graph wrapping with an ambient-view branch.
When no explicit graph argument is given but the scope bound a view,
wrap the procedure's `mgp_graph` in the accessor matching the view: a
SubgraphDbAccessor for a project() subgraph, a VirtualGraphDbAccessor
for a virtualGraph/derive projection, and the real graph for the
identity view. The wrapper borrows the scope-owned graph rather than
copying it. An explicit graph argument is still checked first, so it
takes precedence over the ambient view.
The accessor types are reached through the existing mgp dispatch, so
already-compiled read procedures iterate vertices and expand edges over
the bound view with no source change. The views expose their borrowed
graph through a getter for this wrapping; VirtualGraphView now holds its
projection by non-const pointer, matching the non-const graph the bind
site already owns.
e2e covers iterate and expand over both a virtualGraph projection and a
project() subgraph, the real graph outside any scope, and explicit-
argument precedence over the ambient view.
AC2 requires an unmodified procedure to expand edges over a project() subgraph bound by USE, not only iterate its vertices. Add the missing case: a member node's out-edge is traversed through the ambient subgraph accessor inside a USE scope.
Isolate the per-row cost the GraphView seam adds to an unlabeled full scan. RawIterate reads the real graph through VerticesIterable directly, the path a scan took before the seam and the path a label scan still takes. ViaVertexRange reads the same vertices through VertexRange, the variant the seam interposes once the identity view is bound, mirroring the cursor's deref and write visits. The delta between the two is the variant tax, so the seam's per-row overhead can be measured rather than asserted in a comment.
Record three issues found reviewing the USE-scope read path, each to be driven test-first. A filtered match inside a USE scope can resolve against a real-graph index and read the real graph instead of the bound projection, because the body is planned with ordinary rules and the view boundary is wrapped around the finished plan. A derive() overlay reads unmodified properties through to its origin vertex, and a scanned projection node is treated as always visible, so fine-grained authorization on the origin may be bypassed. Ambient degree answers silently for view and element pairings the scan path is not expected to produce, where the expand path asserts. Scoped to the planner path that supports USE; the plan_v2 rejection is left in place.
A label, property, or id predicate inside a CALL { USE g ... } body must
read the bound ambient graph, never a real-graph index. The rewriter
boundary already guarantees this: the USE body is planned as a raw
ScanAll + Filter and wrapped in BindGraphView, and the index, edge-index,
and join rewriters do not descend past that boundary, so no index scan is
substituted into the body for any scan flavour.
Add the regression coverage that pins this down beyond the existing label
case: label+property equality and range against a real composite index
over a projection, and an id predicate over a project() subgraph where a
real vertex outside the subgraph must not be reachable by its id while a
member is. The id ids are read back and passed as parameters so the test
does not depend on synthetic-gid assignment.
The tests fail if the BindGraphView boundary is removed from the
rewriters; no production change was needed.
Fine-grained authorization gates vertex reads by label: a user denied a label cannot read those vertices. The scan-visibility check enforced this for real vertices but waved every projection node through unconditionally, so an overlay node that reads through to a real origin vertex bypassed the label check its origin would face. A scan over such an overlay could expose an origin's labels and read-through properties the user is denied. Give an overlay node (one carrying an origin) the same READ visibility decision as its origin; a synthetic node carries no origin and no real-graph data, so it stays always-visible. Filtering at the scan covers the read-through: a node that cannot be scanned cannot have its properties or labels read. Fine-grained read authorization in Memgraph is label and edge-type based; there is no per-property read permission, so label-gated vertex visibility is the applicable check here. Enterprise only.
degree, inDegree, and outDegree resolve a node's degree over the ambient
graph view. When a real vertex was imported into a CALL { USE <projection>
... } scope and its degree taken, the function fell through to the
real-graph degree, leaking the real topology into a projection scope. A
real vertex is not a node of a projection, so its degree in the ambient
projection graph is now zero, consistent with the subgraph branch where a
non-member contributes no member edges.
The companion branch, a virtual node with no projection bound, is reachable
as a literal virtualNode() outside any USE scope; it carries no edges, so
its zero degree is correct and is left as is. Neither pairing is asserted
because both are reachable through valid Cypher.
Two read-path guards stopped at the scan seam and left edge expansion open
inside a CALL { USE <projection> ... } scope.
Fine-grained authorization: an overlay node surfaces through expansion as
well as through a scan, but only the scan checked the overlay's origin
visibility. The virtual-edge expansion arms now apply the same check
(shared as OverlayNodeVisible), so an overlay over a denied origin reached
as an expansion endpoint is dropped just as the scan drops it. Guarding the
scan alone gave a false sense of safety.
Projection topology: a real vertex imported into a projection scope is not
a node of the projection, so its degree was made zero, but expansion still
traversed its real-graph edges, leaking real topology and contradicting the
degree. Expansion now yields nothing for a real vertex in a projection
scope, consistent with its zero degree.
Both are covered by tests that fail without the change: an operator-level
scan-then-expand over a denied overlay, and an expand from an imported real
vertex inside a projection USE scope.
…ation Record the maintainability follow-up surfaced reviewing the projection-scope guards: the view-kind decisions for degree, expansion, and scan visibility are hand-rolled as parallel dynamic_cast cascades, and a new view kind that misses a cascade silently falls through to the real graph. Track centralizing the dispatch behind the GraphView seam so a missing case is caught, not silent.
…ption Records the standing gap that no Bolt client yet consumes the projection provenance channel: the reserved __mg_overlay_ref tag every derive() overlay node carries leaks into generic clients' property maps. The issue scopes the work for mgconsole (suppress reserved __mg_* keys) and Lab (resolve overlay nodes to their projection_schema RUN-header entry and style computed properties distinctly), and notes the wire contract is fixed and non-breaking.
Adds runnable scripts that boot a clean Memgraph from the feat/projection build and drive the projection and write-back surface through mgconsole. run_projection_demo.sh walks the projection acts (with an optional procedures act) plus the guardrail cases; mg.sh is a minimal one-file driver. The .cypher files are the human-readable artifacts; the scripts strip comment-only lines since mgconsole's statement splitter mishandles comments wedged between statements.
The USE-scope operator that wraps a CALL { USE <graph> ... } subquery had
no handler in either the text plan printer or the JSON plan dumper, so
EXPLAIN/PROFILE printed it as the catch-all "Unknown operator!" and the
JSON dump silently dropped it. Both printers now name the operator and
render the USE target expression.
The visitor framework's parameterless DefaultPreVisit cannot name the
offending operator, so a unit test guards against an unhandled operator
regressing the USE-scope plan back to the anonymous fallback.
id() now reports an overlay node's origin id (its identity in the real graph) instead of the overlay's negative synthetic id. A synthetic node, which has no origin, still reports its own negative id, and real entities are unchanged. virtual_id() is the overlay-local identity: the negative synthetic id for a virtual node or edge (synthetic or overlay alike), and null for a real vertex or edge that has no overlay identity. Together the two functions let a caller ask either 'what is this node in the real graph' (id) or 'what is its handle inside the projection, if any' (virtual_id).
A synthetic Gid is unique for the life of the process, so id() of a virtual node depended on how many virtual entities had been made earlier in the server's lifetime: separate queries saw -1, -2, -3 rather than each starting at -1. Internal Gids must stay process-unique to keep identity collision-free, so this maps them to external ids only at the user-facing boundary. EvaluationContext now carries a SyntheticIdMapper, created per query and shared across parallel workers. id()/virtual_id() (and elementId(), which builds on id()) project a virtual entity's Gid through it to a dense negative id assigned on first sight. Within a query the ids are stable and repeated references to one entity correlate to the same number; a new query starts again at -1. Overlay nodes are unaffected: id() still reports the origin's real id. Serialization of a returned virtual node over Bolt still exposes the raw synthetic Gid; routing that through the same mapper is a follow-up.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




No description provided.