Skip to content

feat(rel): unlabelled MATCH (n) RETURN n returns a whole node value (#889 slice 1)#893

Merged
DecisionNerd merged 1 commit into
mainfrom
feature/889-node-value-completion
Jun 23, 2026
Merged

feat(rel): unlabelled MATCH (n) RETURN n returns a whole node value (#889 slice 1)#893
DecisionNerd merged 1 commit into
mainfrom
feature/889-node-value-completion

Conversation

@DecisionNerd

@DecisionNerd DecisionNerd commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Part of #889 (slice 1 of 3 — the engine; does not close the issue).

Summary

#785 materialized a whole node value only when the label was written in the pattern (MATCH (n:Person) RETURN n); an unlabelled MATCH (n) RETURN n fell back to a uuid-only struct (no label, no properties). This slice resolves both at read time, so an unlabelled match returns the node with its real label name and properties.

How

Label name (recovered from the node's stored type_id):

  • RuntimeCatalog gains entity_type_name / entity_type_names_with_ids — the entity-type (label) mirror of the existing relation_type_name* reverse lookups — surfaced through GraphCatalog::label_names.
  • The lowerer keeps type_id_to_entity_name ontology-only (it drives property-table routing, which in exploratory mode must read _untyped) and builds a separate label-name map (ontology + runtime catalog) used only for node-value rendering. node_value_struct emits a runtime CASE type_id WHEN k THEN 'Label' … ELSE NULL END (ascending key order → deterministic plan) for the labels field when the pattern had no label.

Properties:

  • prop_table_stem centralizes property-file selection: an unlabelled node in exploratory mode now resolves to _untyped (where all exploratory properties are written), so the node value carries its props; an unlabelled node in an ontology mode stays a no-op (properties are spread across per-entity tables — a multi-table union, out of scope).

Scope / deferrals

Validation

  • New e2e unlabelled_match_returns_node_with_resolved_label_and_propsMATCH (n) WHERE n.name='Alice' RETURN n returns Struct{labels: ['Person'], name: 'Alice', …} (76 pass).
  • gf-rel goldens (incl. the exploratory property-join golden), gf-ir, gf-storage suites green; cargo clippy --workspace -- -D warnings clean; fmt clean.

🤖 Generated with Claude Code

Note

Return real label and properties for unlabelled MATCH (n) RETURN n queries

  • Unlabelled node scans now resolve the node's label at runtime by building a CASE expression over type_id values sourced from RuntimeCatalog, replacing the previous NULL placeholder.
  • Property columns for unlabelled scans in exploratory mode are now read from the _untyped property table via a LEFT join, making var_N.<prop> columns available downstream.
  • GraphCatalog now carries a TypeId→label-name reverse map (built from RuntimeCatalog::entity_type_names_with_ids) and exposes it via label_names() for use during expression lowering.
  • ExprLowerer accepts the label-name map through the updated with_prop_names_and_nodes constructor and passes it into node_value_struct to populate the labels field.
  • Behavioral Change: queries like MATCH (n) WHERE n.name = 'Alice' RETURN n now return a structured node value with a resolved label and properties instead of a node with null labels.

Macroscope summarized d3345fb.

Summary by CodeRabbit

  • Bug Fixes
    • Unlabelled node queries now correctly resolve and return node labels from type information while preserving properties, improving query result completeness in exploratory mode.

…889)

Slice 1 of #889. #785 materialized a whole node value only when the
label was written in the pattern; an unlabelled `MATCH (n) RETURN n`
fell back to a uuid-only struct (no label, no props). This resolves both
at read time.

Label name:
- RuntimeCatalog gains `entity_type_name` / `entity_type_names_with_ids`
  (mirror of the relation-type reverse lookups), surfaced through
  GraphCatalog::label_names.
- The lowerer keeps `type_id_to_entity_name` ontology-only (it drives
  property-table routing, which in exploratory mode must read `_untyped`)
  and builds a SEPARATE label-name map (ontology + runtime catalog) for
  node-value rendering only. node_value_struct emits a runtime
  `CASE type_id WHEN k THEN 'Label' … ELSE NULL` (ascending key order,
  deterministic) for the `labels` field when the pattern had no label.

Properties:
- `prop_table_stem` centralizes property-file selection: an unlabelled
  node in exploratory mode now resolves to `_untyped` (where all
  exploratory properties are written), so the node value carries its
  props; an unlabelled node in an ontology mode stays a no-op (props are
  spread across per-entity tables — a multi-table union, out of scope).

One label per node (multi-label is #799). TCK node-value rendering +
scenario un-skipping is slice 2.

Validated: new e2e `unlabelled_match_returns_node_with_resolved_label_and_props`
(76 pass); gf-rel goldens (incl. the exploratory property-join golden) and
gf-ir/gf-storage suites green; clippy --workspace -D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c13b7156-0ea0-4517-8167-4801bc03f95b

📥 Commits

Reviewing files that changed from the base of the PR and between 9b91323 and d3345fb.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/*.md
📒 Files selected for processing (5)
  • crates/gf-api/tests/e2e_baseline.rs
  • crates/gf-ir/src/catalog.rs
  • crates/gf-rel/src/expr.rs
  • crates/gf-rel/src/lowerer.rs
  • crates/gf-storage/src/catalog.rs

Walkthrough

Adds runtime resolution of node labels for unlabelled MATCH (n) queries. RuntimeCatalog gains reverse-lookup methods for entity-type IDs; GraphCatalog builds a label_names map from them. ExprLowerer uses the map to emit a CASE expression over base.type_id for labels, and GraphPlanLowerer routes unlabelled property joins to the _untyped table in exploratory mode.

Changes

Unlabelled Node Label Resolution

Layer / File(s) Summary
RuntimeCatalog entity-type reverse-lookup
crates/gf-ir/src/catalog.rs
Adds entity_type_name(RuntimeTypeId) -> Option<&str> and entity_type_names_with_ids() -> impl Iterator<Item = (RuntimeTypeId, &str)> to RuntimeCatalog for resolving stored type IDs back to label names.
GraphCatalog label_names field and builder
crates/gf-storage/src/catalog.rs
Adds label_names: HashMap<u32, String> field to GraphCatalog, populated by a new build_label_names helper during open(), with a public label_names() accessor.
ExprLowerer CASE-based label generation
crates/gf-rel/src/expr.rs
Adds type_id_to_entity_name map field to ExprLowerer; node_value_struct emits a sorted CASE over base.type_id for label resolution when the bind-time label is absent and the map is non-empty. New type_id_label_case helper builds this expression.
GraphPlanLowerer label map wiring and prop_table_stem
crates/gf-rel/src/lowerer.rs
expr_lowerer merges ontology and catalog label names into a unified map passed to ExprLowerer. New prop_table_stem helper routes unlabelled exploratory node scans to the "_untyped" property table; node_prop_cols and join_node_properties are updated accordingly.
E2E baseline test
crates/gf-api/tests/e2e_baseline.rs
New test unlabelled_match_returns_node_with_resolved_label_and_props verifies that an unlabelled MATCH (n) returns a node Struct with labels=["Person"] and name="Alice" in Arrow output.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • DecisionNerd/graphforge#684: The new entity_type_name and entity_type_names_with_ids methods in RuntimeCatalog extend the RuntimeCatalog implementation introduced in this PR.
  • DecisionNerd/graphforge#691: Both PRs modify ExprLowerer in crates/gf-rel/src/expr.rs; PR #691 introduced the lowerer, this PR extends it with type_id_to_entity_name and CASE-based label generation.
  • DecisionNerd/graphforge#694: Both PRs modify GraphPlanLowerer in crates/gf-rel/src/lowerer.rs; PR #694 introduced the lowerer, this PR extends it with prop_table_stem and unlabelled property-join routing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: enabling unlabelled MATCH (n) RETURN n to return complete node values with resolved labels and properties.
Description check ✅ Passed The description is comprehensive, covering summary, implementation details, scope/deferrals, and validation; however, several template sections remain unfilled (Type of Change, Changes Made, Testing details, and most checklist items).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/889-node-value-completion

Comment @coderabbitai help to get the list of available commands.

@DecisionNerd DecisionNerd merged commit 536eae4 into main Jun 23, 2026
41 checks passed
@DecisionNerd DecisionNerd deleted the feature/889-node-value-completion branch June 23, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant