Query the in-memory graph with Cypher#868
Conversation
cb17dab to
2e6a202
Compare
2e6a202 to
bc2a231
Compare
vinistock
left a comment
There was a problem hiding this comment.
Still trying to wrap my head around the entire engine, but left some comments already. Excited to have a unified way of querying the graph.
I wonder if there's some IRB trick we can use to enter a "query" mode that accepts the Cypher queries directly (non-valid Ruby). Something like:
bundle exec rdx -i
Indexing...
Resolving...
> graph["Foo"]
=> <Declaration ...>
>
> query_mode!
> MATCH (n:Class|Module) RETURN n.name ORDER BY n.name
=> [Foo]
Great idea. I am prototyping what's possible here, but obviously it won't be a part of this PR. |
|
Ok, this PR has a prototype for the console mode extension: #883 |
f6e2615 to
7e08a7b
Compare
|
@vinistock If it is helpful, I had this diagram generated from the codebase for how the query engine works at a high level:
|
Morriar
left a comment
There was a problem hiding this comment.
Why did we need our own implementation of the Cypher parser? None of the existing Rust implementations could be reused?
Could you clean the commit history to make this PR easier to read? First commit seems to introduce a whole parser that gets removed later.
The existing implementations are either unmaintained, or were doing much more than what we needed, or were pulling in a lot of dependencies. I wanted to start with a parser under our control and with no dependencies, that support only the subset of the syntax that we want to support (e.g. we will probably never need If needed, we can swap what we have with another library down the line, but this seems to work just fine.
Sure, I can do that tomorrow. |
vinistock
left a comment
There was a problem hiding this comment.
Just some minor things, but I think we can ship and iterate on it.
Note: it seems that, like query.rs, cypher queries are going to be quite heavy on graph traversal (depending on what the user is searching for of course). If possible, it would be nice to scan the graph in parallel
ded5eb9 to
4fa3719
Compare
Expose the graph through Cypher — the de facto standard graph query language — so new introspection needs become queries instead of bespoke APIs. Queries are read-only and run directly against the in-memory `Graph`. The engine (lexer, parser, executor, formatting) lives in the standalone `cypher-parser` crate, generic over a `GraphProvider` trait. This adds the rubydex-specific pieces: - `query::cypher::schema` — `impl GraphProvider for Graph`: the property graph mapping (node labels, relationship types, properties) with the relationship catalog defined once in a `REL_SCHEMAS` table. - `query::cypher::schema_info` — renders that model for discoverability. - `Document::file_path`/`file_name` for the Document `path`/`name` properties, decoding `file://` URIs via the `url` crate.
Add `--query <CYPHER>`, `--schema`, and `--format table|json` to `rubydex_cli`. The query is parsed up front — before listing/indexing — so a malformed query fails fast instead of after a full workspace build, and `--schema` prints the static catalog without building a graph at all. clap constraints reject nonsensical combinations: `--query`/`--schema` are mutually exclusive, `--format` requires one of them, and `--show-builtins` requires `--dot`.
9dfbbc1 to
85bbbba
Compare
Add an opaque `Rubydex::Query` object backed by new FFI exports: - `Rubydex::Query.parse(str)` parses without a graph, raising ArgumentError on a syntax error, so a query can be validated before indexing. - `Query#render(graph, format)` runs a parsed query and returns the formatted output; a parsed query is reusable across graphs. - `Rubydex::Query.schema(format)` describes the queryable schema. The Query class lives in its own `ext/rubydex/query.c`, with the String/Symbol/nil format coercion in a shared `rdxi_symbol_or_string_cstr` utils helper. The `rdx` CLI gains `query`/`console` commands (with `query --schema`): graph-independent switches return early, then the workspace graph is built once and the chosen command runs against it.
85bbbba to
cc553f6
Compare
|
@vinistock and @Morriar: I think I've addressed all the comments and I have a clean commit history now. Can you both do a final review? |

Goal
Give clients a flexible, future-proof way to query the in-memory graph using Cypher — the de facto standard graph query language. Instead of adding a bespoke method for every traversal, this exposes the graph through a query language clients already know, so new introspection needs become queries rather than new APIs. Queries are read-only, run directly against the existing in-memory
Graph(no duplication, no embedded database).References
Architecture
The Cypher engine itself — lexer, recursive-descent parser, AST, the tree-walking executor, values, and result formatting — lives in a separate, published crate,
cypher-parser. The executor is generic over aGraphProvidertrait, so it has no dependency on rubydex.rubydexdepends oncypher-parserand provides only the rubydex-specific pieces:query::cypher::schema—impl GraphProvider for Graph, the property-graph mapping.query::cypher::schema_info— the static schema description.How it's exposed
rubydex_cli:--query "<CYPHER>",--schema,--format table|json.Rubydex::Query:Rubydex::Query.parse(str)→ an opaque, reusable parsed query (raisesArgumentErroron syntax errors, needs no graph).Rubydex::Query#render(graph, format = :table)→ runs a parsed query against a graph and returns the formatted output (table or JSON).Rubydex::Query.schema(format = :table)→ describes the queryable schema.rdxcommand CLI:Parse first, then build the graph
Both CLIs parse the query into the opaque parsed object before indexing/resolution, so a malformed query fails fast (~0.1s) instead of after a full workspace index:
A parsed
Rubydex::Queryis reusable: parse once, run against many graphs.Graph schema exposed to queries
rdx query --schema/rubydex_cli --schema/Rubydex::Query.schemaprint this model:Node labels:
Document,Definition,Declaration, the grouping labelNamespace, and declaration kind sub-labels (Class,Module,SingletonClass,Method,Constant,ConstantAlias,GlobalVariable,InstanceVariable,ClassVariable).Relationship types:
DEFINES(Document→Definition),DECLARES(Definition→Declaration),CONTAINS(lexical nesting),HAS_PARENT(direct superclass; reverse-traverse for direct subclasses,*for the chain),INCLUDES/PREPENDS/EXTENDS(mixins),OWNS(members),HAS_ANCESTOR(linearized ancestor chain, incl. modules),HAS_DESCENDANT(reverse ofHAS_ANCESTOR),REFERENCES(Document→Declaration).Properties: Declaration:
name,unqualified_name,kind,visibility,definition_count; Definition:kind,name,file,line; Document:uri,path(file-system path),name(base file name).Supported syntax:
MATCH(node patterns with label disjunction:A|Band inline properties; relationship patterns with direction, type lists, and variable length*min..max),WHERE(=, <>, <, <=, >, >=, CONTAINS, STARTS WITH, ENDS WITH,AND/OR/NOT),RETURN(DISTINCT,AS, aggregatescount/collect/min/max/sum/avg),ORDER BY,SKIP,LIMIT. Read-only; write clauses are intentionally unsupported.Try it
From Ruby:
What's here
Core work:
cypher-parsercrate (generic overGraphProvider).GraphProvidermapping plus a single-source schema catalog (RelType+NODE_LABELS/NODE_PROPERTIES), rendered byschema_info.--query/--schemaonrubydex_cliand ardx query/consolecommand CLI, parsing the query up front so a syntax error fails fast before indexing.Review refinements folded in: direction-correct
HAS_PARENT/HAS_ANCESTOR/HAS_DESCENDANTedge naming,rdx query --schemaas a flag, a single "build graph once, then dispatch" CLI structure, extraction ofRubydex::Queryintoquery.cand a sharedutilshelper, robustfile://path handling, and clap validation that rejects nonsensical flag combinations.