rubydex 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +34 -1
  4. data/exe/rdx +131 -55
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/graph.c +14 -3
  8. data/ext/rubydex/query.c +105 -0
  9. data/ext/rubydex/query.h +8 -0
  10. data/ext/rubydex/reference.c +60 -0
  11. data/ext/rubydex/rubydex.c +2 -0
  12. data/ext/rubydex/utils.c +12 -0
  13. data/ext/rubydex/utils.h +5 -0
  14. data/lib/rubydex/version.rb +1 -1
  15. data/rbi/rubydex.rbi +22 -0
  16. data/rust/Cargo.lock +7 -0
  17. data/rust/rubydex/Cargo.toml +1 -0
  18. data/rust/rubydex/benches/graph_memory.rs +22 -4
  19. data/rust/rubydex/src/compile_assertions.rs +15 -0
  20. data/rust/rubydex/src/diagnostic.rs +1 -1
  21. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  22. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  23. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  24. data/rust/rubydex/src/main.rs +2 -124
  25. data/rust/rubydex/src/model/declaration.rs +0 -11
  26. data/rust/rubydex/src/model/definitions.rs +27 -26
  27. data/rust/rubydex/src/model/document.rs +43 -7
  28. data/rust/rubydex/src/model/graph.rs +40 -28
  29. data/rust/rubydex/src/model/id.rs +55 -0
  30. data/rust/rubydex/src/model/ids.rs +21 -9
  31. data/rust/rubydex/src/model/name.rs +35 -7
  32. data/rust/rubydex/src/model/references.rs +16 -13
  33. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  34. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  35. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  36. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  37. data/rust/rubydex/src/query/cypher.rs +57 -0
  38. data/rust/rubydex/src/query.rs +2 -0
  39. data/rust/rubydex/src/resolution.rs +248 -227
  40. data/rust/rubydex/src/resolution_tests.rs +263 -65
  41. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  42. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  43. data/rust/rubydex-sys/src/graph_api.rs +158 -14
  44. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  45. metadata +8 -2
@@ -0,0 +1,161 @@
1
+ //! Renders the Cypher property-graph model — node labels, relationship types, and node properties —
2
+ //! for the `--schema` output. The model itself is defined once in [`super::schema`] (labels and
3
+ //! properties as `NODE_LABELS`/`NODE_PROPERTIES`, relationships as [`RelType`]); this module only
4
+ //! formats it, so there is a single source of truth.
5
+
6
+ use cypher_parser::OutputFormat;
7
+ use cypher_parser::value::write_json_string;
8
+
9
+ use super::schema::{NODE_LABELS, NODE_PROPERTIES, RelType};
10
+
11
+ /// Renders the schema catalog in the requested format.
12
+ #[must_use]
13
+ pub fn describe(format: OutputFormat) -> String {
14
+ match format {
15
+ OutputFormat::Table => render_table(),
16
+ OutputFormat::Json => render_json(),
17
+ }
18
+ }
19
+
20
+ fn render_table() -> String {
21
+ let mut out = String::new();
22
+
23
+ out.push_str("Node labels\n");
24
+ let label_rows: Vec<[&str; 3]> = NODE_LABELS
25
+ .iter()
26
+ .map(|l| [l.label, l.matches, l.description])
27
+ .collect();
28
+ push_table(&mut out, &["Label", "Matches", "Description"], &label_rows);
29
+
30
+ out.push_str("\nRelationship types\n");
31
+ let rel_rows: Vec<[&str; 4]> = RelType::all()
32
+ .map(|rel| {
33
+ let schema = rel.schema();
34
+ [schema.name, schema.from, schema.to, schema.description]
35
+ })
36
+ .collect();
37
+ push_table(&mut out, &["Type", "From", "To", "Description"], &rel_rows);
38
+
39
+ out.push_str("\nProperties\n");
40
+ let prop_rows: Vec<[&str; 3]> = NODE_PROPERTIES
41
+ .iter()
42
+ .map(|p| [p.node_type, p.property, p.description])
43
+ .collect();
44
+ push_table(&mut out, &["Node type", "Property", "Description"], &prop_rows);
45
+
46
+ out
47
+ }
48
+
49
+ /// Renders a single aligned table section. `N` is the column count.
50
+ fn push_table<const N: usize>(out: &mut String, headers: &[&str; N], rows: &[[&str; N]]) {
51
+ let mut widths: [usize; N] = std::array::from_fn(|i| headers[i].chars().count());
52
+ for row in rows {
53
+ for (index, cell) in row.iter().enumerate() {
54
+ widths[index] = widths[index].max(cell.chars().count());
55
+ }
56
+ }
57
+
58
+ push_table_row(out, headers, &widths);
59
+ for (index, width) in widths.iter().enumerate() {
60
+ if index > 0 {
61
+ out.push_str("-+-");
62
+ }
63
+ for _ in 0..*width {
64
+ out.push('-');
65
+ }
66
+ }
67
+ out.push('\n');
68
+ for row in rows {
69
+ push_table_row(out, row, &widths);
70
+ }
71
+ }
72
+
73
+ fn push_table_row<const N: usize>(out: &mut String, cells: &[&str; N], widths: &[usize; N]) {
74
+ for (index, width) in widths.iter().enumerate() {
75
+ if index > 0 {
76
+ out.push_str(" | ");
77
+ }
78
+ let cell = cells[index];
79
+ out.push_str(cell);
80
+ for _ in 0..width.saturating_sub(cell.chars().count()) {
81
+ out.push(' ');
82
+ }
83
+ }
84
+ out.push('\n');
85
+ }
86
+
87
+ fn render_json() -> String {
88
+ let mut out = String::from("{\"node_labels\":[");
89
+ for (index, label) in NODE_LABELS.iter().enumerate() {
90
+ if index > 0 {
91
+ out.push(',');
92
+ }
93
+ out.push_str("{\"label\":");
94
+ write_json_string(&mut out, label.label);
95
+ out.push_str(",\"matches\":");
96
+ write_json_string(&mut out, label.matches);
97
+ out.push_str(",\"description\":");
98
+ write_json_string(&mut out, label.description);
99
+ out.push('}');
100
+ }
101
+
102
+ out.push_str("],\"relationships\":[");
103
+ for (index, rel) in RelType::all().enumerate() {
104
+ if index > 0 {
105
+ out.push(',');
106
+ }
107
+ let schema = rel.schema();
108
+ out.push_str("{\"type\":");
109
+ write_json_string(&mut out, schema.name);
110
+ out.push_str(",\"from\":");
111
+ write_json_string(&mut out, schema.from);
112
+ out.push_str(",\"to\":");
113
+ write_json_string(&mut out, schema.to);
114
+ out.push_str(",\"description\":");
115
+ write_json_string(&mut out, schema.description);
116
+ out.push('}');
117
+ }
118
+
119
+ out.push_str("],\"properties\":[");
120
+ for (index, prop) in NODE_PROPERTIES.iter().enumerate() {
121
+ if index > 0 {
122
+ out.push(',');
123
+ }
124
+ out.push_str("{\"node_type\":");
125
+ write_json_string(&mut out, prop.node_type);
126
+ out.push_str(",\"property\":");
127
+ write_json_string(&mut out, prop.property);
128
+ out.push_str(",\"description\":");
129
+ write_json_string(&mut out, prop.description);
130
+ out.push('}');
131
+ }
132
+
133
+ out.push_str("]}");
134
+ out
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use super::*;
140
+
141
+ #[test]
142
+ fn table_lists_labels_relationships_and_properties() {
143
+ let output = describe(OutputFormat::Table);
144
+ assert!(output.contains("Node labels"));
145
+ assert!(output.contains("Relationship types"));
146
+ assert!(output.contains("Properties"));
147
+ assert!(output.contains("Namespace"));
148
+ assert!(output.contains("HAS_PARENT"));
149
+ assert!(output.contains("unqualified_name"));
150
+ }
151
+
152
+ #[test]
153
+ fn json_is_well_formed_object() {
154
+ let output = describe(OutputFormat::Json);
155
+ assert!(output.starts_with("{\"node_labels\":["));
156
+ assert!(output.contains("\"relationships\":["));
157
+ assert!(output.contains("\"properties\":["));
158
+ assert!(output.contains("\"type\":\"DEFINES\""));
159
+ assert!(output.ends_with("]}"));
160
+ }
161
+ }
@@ -0,0 +1,228 @@
1
+ use super::run_query;
2
+ use super::schema::RelType;
3
+ use crate::model::graph::Graph;
4
+ use crate::test_utils::GraphTest;
5
+ use cypher_parser::{CypherValue, OutputFormat, ResultSet, execute, parse};
6
+
7
+ #[test]
8
+ fn relationship_metadata_is_self_consistent() {
9
+ // Guards the single source of truth: every relationship's catalog name must round-trip through
10
+ // `parse`, and every field must be populated.
11
+ for rel in RelType::all() {
12
+ let schema = rel.schema();
13
+ assert_eq!(RelType::parse(schema.name), Some(rel));
14
+ assert_eq!(RelType::parse(&schema.name.to_ascii_lowercase()), Some(rel));
15
+ assert!(!schema.from.is_empty(), "{} missing `from`", schema.name);
16
+ assert!(!schema.to.is_empty(), "{} missing `to`", schema.name);
17
+ assert!(!schema.description.is_empty(), "{} missing description", schema.name);
18
+ }
19
+ }
20
+
21
+ // Parser-only tests live in the `cypher-parser` crate. These exercise the executor and the
22
+ // end-to-end query/format path against a real graph.
23
+
24
+ fn fixture_graph() -> Graph {
25
+ let mut context = GraphTest::new();
26
+ context.index_uri(
27
+ "file:///zoo.rb",
28
+ "
29
+ module Walkable
30
+ end
31
+
32
+ class Animal
33
+ def speak; end
34
+ end
35
+
36
+ class Dog < Animal
37
+ include Walkable
38
+ end
39
+
40
+ class Cat < Animal
41
+ end
42
+ ",
43
+ );
44
+ context.resolve();
45
+ context.into_graph()
46
+ }
47
+
48
+ fn run(graph: &Graph, query: &str) -> ResultSet {
49
+ let parsed = parse(query).unwrap();
50
+ execute(graph, &parsed).unwrap()
51
+ }
52
+
53
+ fn column_strings(result: &ResultSet, column: usize) -> Vec<String> {
54
+ let mut values: Vec<String> = result.rows.iter().map(|row| row[column].to_display_string()).collect();
55
+ values.sort();
56
+ values
57
+ }
58
+
59
+ #[test]
60
+ fn scans_declarations_by_label_and_property() {
61
+ let graph = fixture_graph();
62
+ let result = run(&graph, "MATCH (c:Class {name: 'Dog'}) RETURN c.name");
63
+ assert_eq!(result.columns, vec!["c.name".to_string()]);
64
+ assert_eq!(column_strings(&result, 0), vec!["Dog".to_string()]);
65
+ }
66
+
67
+ #[test]
68
+ fn scans_label_disjunction() {
69
+ let graph = fixture_graph();
70
+ let result = run(
71
+ &graph,
72
+ "MATCH (n:Class|Module) WHERE n.name = 'Animal' OR n.name = 'Walkable' RETURN n.name, n.kind",
73
+ );
74
+ let names = column_strings(&result, 0);
75
+ assert_eq!(names, vec!["Animal".to_string(), "Walkable".to_string()]);
76
+ }
77
+
78
+ #[test]
79
+ fn follows_inherits_relationship() {
80
+ let graph = fixture_graph();
81
+ let result = run(
82
+ &graph,
83
+ "MATCH (c:Class)-[:HAS_PARENT]->(p:Class) WHERE c.name = 'Dog' RETURN p.name",
84
+ );
85
+ assert_eq!(column_strings(&result, 0), vec!["Animal".to_string()]);
86
+ }
87
+
88
+ #[test]
89
+ fn follows_incoming_relationship() {
90
+ let graph = fixture_graph();
91
+ let result = run(
92
+ &graph,
93
+ "MATCH (p:Class)<-[:HAS_PARENT]-(c:Class) WHERE p.name = 'Animal' RETURN c.name",
94
+ );
95
+ assert_eq!(column_strings(&result, 0), vec!["Cat".to_string(), "Dog".to_string()]);
96
+ }
97
+
98
+ #[test]
99
+ fn follows_includes_relationship() {
100
+ let graph = fixture_graph();
101
+ let result = run(
102
+ &graph,
103
+ "MATCH (c:Class)-[:INCLUDES]->(m) WHERE c.name = 'Dog' RETURN m.name",
104
+ );
105
+ assert_eq!(column_strings(&result, 0), vec!["Walkable".to_string()]);
106
+ }
107
+
108
+ #[test]
109
+ fn follows_owns_to_method() {
110
+ let graph = fixture_graph();
111
+ let result = run(
112
+ &graph,
113
+ "MATCH (c:Class)-[:OWNS]->(m:Method) WHERE c.name = 'Animal' RETURN m.unqualified_name",
114
+ );
115
+ assert!(column_strings(&result, 0).iter().any(|name| name.contains("speak")));
116
+ }
117
+
118
+ #[test]
119
+ fn variable_length_ancestor_chain() {
120
+ let graph = fixture_graph();
121
+ let result = run(
122
+ &graph,
123
+ "MATCH (c:Class)-[:HAS_ANCESTOR]->(a) WHERE c.name = 'Dog' RETURN a.name",
124
+ );
125
+ let ancestors = column_strings(&result, 0);
126
+ assert!(ancestors.contains(&"Animal".to_string()));
127
+ assert!(ancestors.contains(&"Walkable".to_string()));
128
+ assert!(ancestors.contains(&"Object".to_string()));
129
+ }
130
+
131
+ #[test]
132
+ fn traverses_document_to_declaration() {
133
+ let graph = fixture_graph();
134
+ let result = run(
135
+ &graph,
136
+ "MATCH (d:Document)-[:DEFINES]->(def:Definition)-[:DECLARES]->(decl) WHERE decl.name = 'Dog' RETURN decl.name",
137
+ );
138
+ assert_eq!(column_strings(&result, 0), vec!["Dog".to_string()]);
139
+ }
140
+
141
+ #[test]
142
+ fn aggregation_counts_subclasses() {
143
+ let graph = fixture_graph();
144
+ let result = run(
145
+ &graph,
146
+ "MATCH (c:Class)-[:HAS_PARENT]->(p:Class) WHERE p.name = 'Animal' RETURN p.name, count(c) AS subclasses",
147
+ );
148
+ assert_eq!(result.rows.len(), 1);
149
+ assert_eq!(result.rows[0][0], CypherValue::Str("Animal".into()));
150
+ assert_eq!(result.rows[0][1], CypherValue::Int(2));
151
+ }
152
+
153
+ #[test]
154
+ fn distinct_and_order_and_limit() {
155
+ let graph = fixture_graph();
156
+ let result = run(
157
+ &graph,
158
+ "MATCH (c:Class)-[:HAS_PARENT]->(p:Class) RETURN DISTINCT p.name ORDER BY p.name LIMIT 1",
159
+ );
160
+ assert_eq!(result.rows.len(), 1);
161
+ assert_eq!(result.rows[0][0], CypherValue::Str("Animal".into()));
162
+ }
163
+
164
+ #[test]
165
+ fn where_with_boolean_operators() {
166
+ let graph = fixture_graph();
167
+ let result = run(
168
+ &graph,
169
+ "MATCH (c:Class) WHERE c.name = 'Dog' OR c.name = 'Cat' RETURN c.name",
170
+ );
171
+ assert_eq!(column_strings(&result, 0), vec!["Cat".to_string(), "Dog".to_string()]);
172
+ }
173
+
174
+ #[test]
175
+ fn run_query_table_output() {
176
+ let graph = fixture_graph();
177
+ let output = run_query(
178
+ &graph,
179
+ "MATCH (c:Class {name: 'Dog'}) RETURN c.name",
180
+ OutputFormat::Table,
181
+ )
182
+ .unwrap();
183
+ assert!(output.contains("c.name"));
184
+ assert!(output.contains("Dog"));
185
+ assert!(output.contains("1 row"));
186
+ }
187
+
188
+ #[test]
189
+ fn run_query_json_output() {
190
+ let graph = fixture_graph();
191
+ let output = run_query(
192
+ &graph,
193
+ "MATCH (c:Class {name: 'Dog'}) RETURN c.name",
194
+ OutputFormat::Json,
195
+ )
196
+ .unwrap();
197
+ assert_eq!(output, "[{\"c.name\":\"Dog\"}]");
198
+ }
199
+
200
+ #[test]
201
+ fn unknown_relationship_type_errors() {
202
+ let graph = fixture_graph();
203
+ let parsed = parse("MATCH (a)-[:BOGUS]->(b) RETURN a").unwrap();
204
+ assert!(execute(&graph, &parsed).is_err());
205
+ }
206
+
207
+ #[test]
208
+ fn document_uri_path_and_name_are_distinct() {
209
+ let graph = fixture_graph();
210
+ let result = run(
211
+ &graph,
212
+ "MATCH (d:Document) WHERE d.uri = 'file:///zoo.rb' RETURN d.uri, d.path, d.name",
213
+ );
214
+ assert_eq!(
215
+ result.columns,
216
+ vec!["d.uri".to_string(), "d.path".to_string(), "d.name".to_string()]
217
+ );
218
+ // `uri` is the full URI and `name` is the basename on every platform.
219
+ assert_eq!(column_strings(&result, 0), vec!["file:///zoo.rb".to_string()]);
220
+ assert_eq!(column_strings(&result, 2), vec!["zoo.rb".to_string()]);
221
+
222
+ // `path` is the decoded file-system path. A drive-less `file://` URI has no valid Windows path,
223
+ // so there it falls back to the raw URI; on Unix it decodes to `/zoo.rb`.
224
+ #[cfg(not(windows))]
225
+ assert_eq!(column_strings(&result, 1), vec!["/zoo.rb".to_string()]);
226
+ #[cfg(windows)]
227
+ assert_eq!(column_strings(&result, 1), vec!["file:///zoo.rb".to_string()]);
228
+ }
@@ -0,0 +1,57 @@
1
+ //! A small Cypher query engine that runs read-only queries directly against the in-memory
2
+ //! [`Graph`](crate::model::graph::Graph).
3
+ //!
4
+ //! Supported subset:
5
+ //! - `MATCH` with node patterns `(v:Label {prop: value})` — labels may be a disjunction
6
+ //! (`(v:Class|Module)` matches a node with **any** of the listed labels) — and relationship
7
+ //! patterns `-[:TYPE]->`, `<-[:TYPE]-`, `-[:TYPE]-`, including variable-length `-[:TYPE*min..max]->`.
8
+ //! - `WHERE` with `=`, `<>`, `<`, `<=`, `>`, `>=`, `CONTAINS`, `STARTS WITH`, `ENDS WITH`,
9
+ //! combined with `AND`, `OR`, `NOT`.
10
+ //! - `RETURN` with `DISTINCT`, `AS` aliases, and the aggregates `count`, `collect`, `min`, `max`,
11
+ //! `sum`, `avg`.
12
+ //! - `ORDER BY`, `SKIP`, `LIMIT`.
13
+ //!
14
+ //! See [`schema`] for the node labels and relationship types exposed to queries.
15
+
16
+ // The whole Cypher engine — lexer, parser, AST, executor, values, and formatting — lives in the
17
+ // graph-independent `cypher-parser` crate. rubydex only provides the `GraphProvider` mapping for its
18
+ // `Graph` (in `schema`) and the static schema description (in `schema_info`).
19
+ //
20
+ // `Query` is the opaque parsed-query object: callers can `parse` a query string once (failing fast
21
+ // on syntax errors), then `run_parsed` it against a graph that was built afterwards.
22
+ pub use cypher_parser::{CypherError, OutputFormat, Query, parse};
23
+
24
+ pub mod schema;
25
+ pub mod schema_info;
26
+
27
+ use crate::model::graph::Graph;
28
+
29
+ /// Parses and executes a Cypher query against the graph, returning the formatted output.
30
+ ///
31
+ /// # Errors
32
+ ///
33
+ /// Returns a [`CypherError`] if the query cannot be parsed or executed.
34
+ pub fn run_query(graph: &Graph, query: &str, output_format: OutputFormat) -> Result<String, CypherError> {
35
+ cypher_parser::run_query(graph, query, output_format)
36
+ }
37
+
38
+ /// Executes an already-parsed [`Query`] against the graph and formats the result. Pair with
39
+ /// [`parse`] to validate a query before building the graph.
40
+ ///
41
+ /// # Errors
42
+ ///
43
+ /// Returns a [`CypherError`] if the query cannot be executed.
44
+ pub fn run_parsed(graph: &Graph, query: &Query, output_format: OutputFormat) -> Result<String, CypherError> {
45
+ let result = cypher_parser::execute(graph, query)?;
46
+ Ok(cypher_parser::format::format(&result, output_format))
47
+ }
48
+
49
+ /// Returns a description of the queryable schema (node labels, relationship types, and properties)
50
+ /// in the requested format. The schema is static and does not require a graph.
51
+ #[must_use]
52
+ pub fn schema(output_format: OutputFormat) -> String {
53
+ schema_info::describe(output_format)
54
+ }
55
+
56
+ #[cfg(test)]
57
+ mod tests;
@@ -15,6 +15,8 @@ use crate::model::keywords::{self, Keyword};
15
15
  use crate::model::name::NameRef;
16
16
  use crate::model::visibility::Visibility;
17
17
 
18
+ pub mod cypher;
19
+
18
20
  /// Controls how declaration names are matched against the search query.
19
21
  #[derive(Default)]
20
22
  pub enum MatchMode {