Skip to content

Return Cypher query results as graph objects - #873

Merged
paracycle merged 1 commit into
mainfrom
uk_query_object_results
Jul 29, 2026
Merged

Return Cypher query results as graph objects#873
paracycle merged 1 commit into
mainfrom
uk_query_object_results

Conversation

@paracycle

@paracycle paracycle commented Jun 23, 2026

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.

What changes

  • Rubydex::Query#render(graph, format = :table) — unchanged: rows → formatted String.
  • Rubydex::Query#run(graph) — new: rows → Array<Hash> (T::Array[T::Hash[String, T::untyped]]), where the values are real Ruby objects:
    • a column that binds a node (e.g. RETURN c) comes back as a live Rubydex::Declaration / Definition / Document handle;
    • scalar columns (RETURN c.name, count(c)) come back as plain Ruby values (String, Integer, true/false, nil);
    • a map projection (RETURN c { .name, .kind }) comes back as a Ruby Hash, and a list comes back as a Ruby Array (elements decoded recursively).

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["c.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)-[:HAS_PARENT]->(:Class {name: 'Animal'}) RETURN 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.8.1: the generic executor carries node identity and structured values. CypherValue covers Null/Bool/Int/Str scalars, Node { id, name, .. } for a bound graph node, and List / Map containers (so map projections like c { .name, .kind } survive execution). GraphProvider gains:

    • node_id(node) — encodes a node to an opaque id, so a bound node survives execution as an id rather than being flattened to text;
    • expand_in(node, rel_type) — walks a relationship backwards (incoming edges), enabling queries like (:Definition)<-[:DEFINES]-(:Document).

    The crate stays dependency-free and rubydex-agnostic.

  • rubydex schema (query::cypher::schema): implements node_id and NodeRef::decode over the existing decl:/def:/doc:<u64> id scheme, and implements expand_in for the reversible relationships (DEFINES, DECLARES) so incoming traversals resolve to the right nodes.

  • FFI (rubydex-sys): all Cypher-related code lives in a new cypher_api.rs module — CQueryResult, CParseResult, rdx_cypher_parse, rdx_cypher_query_free, rdx_cypher_schema, and the structured-result types. Only rdx_query_run (the formatted-string runner) remains in graph_api.rs. rdx_query_run_rows returns a CRunRows wrapping a CRowsIter opaque iterator (following the existing DeclarationsIter / DocumentsIter pattern), walked via rdx_rows_iter_{column_count,columns,len,next,free}. CCell is a tagged union (CCellTag: Null/Bool/Int/Str/Node/List/Map); Node cells carry a category (declaration/definition/document) plus the entity id, and Map stores parallel key/value arrays. A cstring_raw helper was added to utils.rs for allocating owned C strings across the FFI boundary.

  • Gem (ext/rubydex/query.c, extending the file added in Query the in-memory graph with Cypher #868): Query#run walks the CRowsIter iterator into an Array<Hash>, decoding each cell — node cells into the appropriate Declaration / Definition / Document handle, lists into Array, maps into Hash, scalars into plain values. The iterator walk is wrapped in rb_ensure so rdx_rows_iter_free runs even if Ruby raises during cell conversion. Both render and run use a new rdxi_graph_from_object helper in graph.h to extract the graph pointer from the Graph VALUE.

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) and supports incoming-edge traversal (expand_in), 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 (-D warnings); cargo fmt --check clean.
  • rake compile green; bundle exec ruby -Itest test/graph_test.rb → 110 runs, 0 failures (includes new object-result, map-projection, aggregation, reverse-traversal, and label-disjunction 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
@paracycle
paracycle force-pushed the uk_query_object_results branch from 9394f29 to e5bb1da Compare June 23, 2026 21:24
@paracycle
paracycle force-pushed the uk_add_cypher_query_engine branch 3 times, most recently from 85bbbba to cc553f6 Compare July 3, 2026 21:56
@paracycle
paracycle force-pushed the uk_query_object_results branch 2 times, most recently from cc69ca7 to d68b816 Compare July 8, 2026 16:40
@paracycle
paracycle force-pushed the uk_add_cypher_query_engine branch from cc553f6 to 8714ea8 Compare July 8, 2026 18:16
@paracycle
paracycle force-pushed the uk_query_object_results branch 2 times, most recently from 51b4f05 to 2bb59e0 Compare July 8, 2026 20:05
@paracycle
paracycle force-pushed the uk_add_cypher_query_engine branch from 57c979c to f99fc2c Compare July 8, 2026 20:17
@paracycle
paracycle force-pushed the uk_query_object_results branch from 2bb59e0 to 1de091c Compare July 8, 2026 20:19
@paracycle
paracycle force-pushed the uk_add_cypher_query_engine branch from f99fc2c to fcbbf98 Compare July 9, 2026 20:09
@paracycle
paracycle force-pushed the uk_query_object_results branch from 1de091c to ae8fcbd Compare July 9, 2026 20:13
@paracycle
paracycle force-pushed the uk_add_cypher_query_engine branch from fcbbf98 to 0206dbe Compare July 9, 2026 20:35
Base automatically changed from uk_add_cypher_query_engine to main July 9, 2026 20:43
@paracycle
paracycle force-pushed the uk_query_object_results branch 3 times, most recently from 5fb40e7 to 933bf3d Compare July 9, 2026 22:39
@paracycle
paracycle requested review from Morriar and vinistock July 10, 2026 16:28
Comment thread ext/rubydex/query.c Outdated
Comment on lines +162 to +166
void *query;
TypedData_Get_Struct(self, void *, &query_type, query);

void *graph;
TypedData_Get_Struct(graph_obj, void *, &graph_type, graph);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use rdxi_graph_from_handle here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rdxi_graph_from_handle is for handle objects (Declaration/Definition/Document) that store a back-reference to their parent Graph via HandleData.graph_obj. In Query#run, graph_obj is the Graph itself, not a handle, so it can't be used directly.

I added rdxi_graph_from_object in graph.h as the Graph-level equivalent, and used it in both render and run.

Comment thread rust/rubydex-sys/src/graph_api.rs Outdated
Comment thread rust/rubydex-sys/src/graph_api.rs Outdated
Comment thread rust/rubydex-sys/src/graph_api.rs Outdated
@paracycle
paracycle force-pushed the uk_query_object_results branch from 933bf3d to a22a461 Compare July 16, 2026 20:18
@paracycle
paracycle requested a review from vinistock July 21, 2026 18:20
Comment thread ext/rubydex/query.c Outdated
@paracycle
paracycle force-pushed the uk_query_object_results branch 2 times, most recently from fa58010 to bdddecc Compare July 29, 2026 17:18
Add an object-returning `Rubydex::Query#run(graph)` alongside the
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 `Declaration`/`Definition`/`Document` handles for node
columns, and Hashes for map projections — so callers can navigate the graph
directly instead of re-parsing formatted text.

Targets `cypher-parser` 0.8:
- structured node results (`CypherValue::Node`) surfaced through the FFI as
  handle-buildable cells, plus `CypherValue::Map` -> Ruby Hash.
- implement the new `GraphProvider::expand_in` for the cheap, exact reverse
  edges (a definition's document, a declaration's definitions), turning
  those incoming traversals from a whole-graph build into direct lookups;
  other edges fall back to the default reverse-adjacency build.
@paracycle
paracycle force-pushed the uk_query_object_results branch from bdddecc to 6865c54 Compare July 29, 2026 17:24
@paracycle
paracycle enabled auto-merge July 29, 2026 17:28
@paracycle
paracycle merged commit 85785e8 into main Jul 29, 2026
47 of 49 checks passed
@paracycle
paracycle deleted the uk_query_object_results branch July 29, 2026 18: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.

3 participants