Skip to content

Return Cypher query results as graph objects#873

Open
paracycle wants to merge 1 commit into
uk_add_cypher_query_enginefrom
uk_query_object_results
Open

Return Cypher query results as graph objects#873
paracycle wants to merge 1 commit into
uk_add_cypher_query_enginefrom
uk_query_object_results

Conversation

@paracycle

Copy link
Copy Markdown
Member

Goal

Build on the Cypher query engine from #868 so callers get matched graph nodes back as first-class objects, not as formatted text they have to re-parse or iterate over.

#868 added Rubydex::Query#render(graph, format), which runs a query and returns a table/json string — great for the CLI and humans, but a dead end for programmatic callers: to actually use a matched class or method you'd have to parse the formatted output, then look the node back up in the graph by name. This PR adds an object-returning sibling, Query#run(graph), that hands you the matched nodes directly.

Stacked on #868. The diff here is only the object-result layer on top of render.

What changes

  • Rubydex::Query#render(graph, format = :table) — unchanged: rows → formatted String.
  • Rubydex::Query#run(graph) — new: rows → Array<Hash>, where the values are real Ruby objects. A column that binds a node (e.g. RETURN n) comes back as a live Rubydex::Declaration / Definition / Document handle; scalar columns come back as plain Ruby values.

So instead of:

# Before: query returns a string; you re-parse it, then re-find the node.
text = query.render(graph, :json)
JSON.parse(text).each do |row|
  decl = graph.find_declaration(row["n.name"])  # extra lookup just to get an object
  decl.definitions.each { ... }
end

you write:

# After: the matched node IS the object. No iteration to re-resolve it.
query = Rubydex::Query.parse("MATCH (c:Class)-[:INHERITS*1..]->(:Class {name: 'ApplicationRecord'}) RETURN DISTINCT c")
query.run(graph).each do |row|
  decl = row["c"]            # => Rubydex::Declaration, already resolved
  decl.definitions           # navigate the graph directly
end

The query language does the matching and filtering; you get the resulting nodes back ready to use, without writing your own traversal/iteration to turn names back into graph objects.

How it works

  • cypher-parser 0.4: the generic executor learns to carry node identity. GraphProvider gains node_id, and CypherValue gains a Node { id, label, name } variant, so a bound node survives execution as an opaque id rather than being flattened to text. (Still dependency-free; rubydex-agnostic.)
  • rubydex schema (query::cypher::schema): implements node_id and NodeRef::decode over the existing decl:/def:/doc:<u64> id scheme, mapping between graph nodes and that opaque id.
  • FFI (rubydex-sys): new rdx_query_run_rows / rdx_result_set_free return a structured CResultSet of CCells (scalars + node handles) instead of a preformatted string.
  • Gem (ext/rubydex/graph.c): Query#run walks the CResultSet into an Array<Hash>, decoding each node cell back into the appropriate Declaration / Definition / Document handle.

Why this matters for both APIs

  • Ruby API: queries become a graph-navigation tool, not just a reporting tool. Match with Cypher, then call methods on the returned handles — no manual name-based re-lookup, no iterating the whole graph to find what the query already found.
  • Rust API: the executor now preserves node identity end-to-end (CypherValue::Node), so any GraphProvider-backed consumer — Rust callers, the FFI layer, future language servers/tools — can get matched nodes back by id and resolve them to their own representation, rather than being limited to formatted strings.

In short: the query does the matching; callers get the nodes, not a transcript of them.

Verification

  • cargo build / cargo test green; clippy clean.
  • rake compile green; rake ruby_test TEST=test/graph_test.rb → 96 runs, 0 failures (includes new object-result tests).

@paracycle paracycle requested a review from a team as a code owner June 23, 2026 21:09
@paracycle paracycle force-pushed the uk_add_cypher_query_engine branch from 2e6a202 to bc2a231 Compare June 23, 2026 21:16
Add an object-returning `Rubydex::Query#run(graph)` alongside the existing
string-returning `Query#render`. Where `render` formats rows into a table
or JSON string, `run` returns an `Array<Hash>` whose values are real Ruby
objects — including `Rubydex::Declaration`/`Definition`/`Document` handles
for node columns — so callers can navigate the graph directly instead of
re-parsing formatted text.

- cypher-parser 0.4: add `GraphProvider#node_id` and a `CypherValue::Node
  { id, label, name }` variant carrying an opaque node id.
- rubydex schema: implement `node_id` and `NodeRef::decode` over the
  `decl:/def:/doc:<u64>` id scheme.
- FFI: add `rdx_query_run_rows`/`rdx_result_set_free` exposing a structured
  `CResultSet`/`CCell` result instead of a preformatted string.
- Gem: `Query#run` builds `Array<Hash>`, decoding node cells back into the
  appropriate Declaration/Definition/Document handles.
@paracycle paracycle force-pushed the uk_query_object_results branch from 9394f29 to e5bb1da Compare June 23, 2026 21:24
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