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.
- checksums.yaml +4 -4
- data/README.md +69 -3
- data/THIRD_PARTY_LICENSES.html +34 -1
- data/exe/rdx +131 -55
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/graph.c +14 -3
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +22 -0
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/main.rs +2 -124
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +40 -28
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +158 -14
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- 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;
|
data/rust/rubydex/src/query.rs
CHANGED
|
@@ -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 {
|