rubydex 0.2.9 → 0.4.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -0,0 +1,791 @@
1
+ //! This file provides the C API for Cypher query parsing, schema, and structured execution.
2
+ //! It connects to the graph through `graph_api::with_graph`.
3
+
4
+ use crate::declaration_api::CDeclaration;
5
+ use crate::definition_api::map_definition_to_kind;
6
+ use crate::graph_api::{GraphPointer, with_graph};
7
+ use crate::utils;
8
+ use libc::{c_char, c_void};
9
+ use rubydex::model::graph::Graph;
10
+ use rubydex::query::cypher::schema::NodeRef;
11
+ use rubydex::query::cypher::{self, CypherValue, OutputFormat};
12
+ use std::ffi::CString;
13
+ use std::ptr;
14
+
15
+ /// Which layer of the Cypher pipeline rejected a call, so callers can raise a matching error class.
16
+ #[repr(C)]
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18
+ pub enum CQueryErrorKind {
19
+ /// No failure: the `error` field of the containing struct is null.
20
+ None,
21
+ /// The caller passed an invalid argument, such as a null pointer or a string that is not UTF-8.
22
+ Argument,
23
+ /// The query text is not valid Cypher.
24
+ Syntax,
25
+ /// The query parsed, but it failed while it ran against the graph.
26
+ Execution,
27
+ }
28
+
29
+ impl CQueryErrorKind {
30
+ fn of(error: &cypher::CypherError) -> Self {
31
+ match error {
32
+ cypher::CypherError::Syntax { .. } => Self::Syntax,
33
+ cypher::CypherError::Execution { .. } => Self::Execution,
34
+ }
35
+ }
36
+ }
37
+
38
+ /// The formatted output of a Cypher result set, or the argument error that prevented it.
39
+ #[repr(C)]
40
+ pub struct CQueryResult {
41
+ /// Non-null on success; null on error. Caller must free with `free_c_string`.
42
+ pub output: *const c_char,
43
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
44
+ pub error: *const c_char,
45
+ }
46
+
47
+ impl CQueryResult {
48
+ #[must_use]
49
+ pub fn success(output: &str) -> Self {
50
+ match CString::new(output) {
51
+ Ok(c_string) => Self {
52
+ output: c_string.into_raw().cast_const(),
53
+ error: ptr::null(),
54
+ },
55
+ Err(_) => Self::error("query output contained an interior NUL byte"),
56
+ }
57
+ }
58
+
59
+ #[must_use]
60
+ pub fn error(message: &str) -> Self {
61
+ Self {
62
+ output: ptr::null(),
63
+ error: utils::cstring_raw(message),
64
+ }
65
+ }
66
+ }
67
+
68
+ /// The result of parsing a Cypher query into an opaque, reusable parsed-query object.
69
+ #[repr(C)]
70
+ pub struct CParseResult {
71
+ /// Non-null on success: a heap-allocated parsed query. Free with `rdx_cypher_query_free`.
72
+ pub query: *mut c_void,
73
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
74
+ pub error: *const c_char,
75
+ /// Which kind of failure `error` describes; `None` when `error` is null.
76
+ pub error_kind: CQueryErrorKind,
77
+ }
78
+
79
+ /// Parses a Cypher query string into an opaque parsed-query object, without needing a graph.
80
+ ///
81
+ /// On success, `query` is a heap-allocated parsed query that can be executed against a graph with
82
+ /// `rdx_query_execute` and must eventually be freed with `rdx_cypher_query_free`. On failure,
83
+ /// `error` holds the message and `error_kind` tells the caller which error to raise.
84
+ ///
85
+ /// # Safety
86
+ ///
87
+ /// - `query` must be a valid, null-terminated UTF-8 string.
88
+ #[unsafe(no_mangle)]
89
+ pub unsafe extern "C" fn rdx_cypher_parse(query: *const c_char) -> CParseResult {
90
+ let Ok(query_str) = (unsafe { utils::convert_char_ptr_to_string(query) }) else {
91
+ return CParseResult {
92
+ query: ptr::null_mut(),
93
+ error: utils::cstring_raw("query is not valid UTF-8"),
94
+ error_kind: CQueryErrorKind::Argument,
95
+ };
96
+ };
97
+
98
+ match cypher::parse(&query_str) {
99
+ Ok(parsed) => CParseResult {
100
+ query: Box::into_raw(Box::new(parsed)).cast::<c_void>(),
101
+ error: ptr::null(),
102
+ error_kind: CQueryErrorKind::None,
103
+ },
104
+ Err(error) => CParseResult {
105
+ query: ptr::null_mut(),
106
+ error: utils::cstring_raw(&error.to_string()),
107
+ error_kind: CQueryErrorKind::of(&error),
108
+ },
109
+ }
110
+ }
111
+
112
+ /// Frees a parsed query previously returned by `rdx_cypher_parse`.
113
+ ///
114
+ /// # Safety
115
+ ///
116
+ /// - `query` must be a pointer returned by `rdx_cypher_parse`, or null. It must not be used after.
117
+ #[unsafe(no_mangle)]
118
+ pub unsafe extern "C" fn rdx_cypher_query_free(query: *mut c_void) {
119
+ if query.is_null() {
120
+ return;
121
+ }
122
+ let _ = unsafe { Box::from_raw(query.cast::<cypher::Query>()) };
123
+ }
124
+
125
+ /// Returns a description of the queryable Cypher schema (node labels, relationship types, and
126
+ /// properties) in the given format (`"table"` or `"json"`). The schema is static and requires no
127
+ /// graph. Caller must free the returned pointer with `free_c_string`.
128
+ ///
129
+ /// # Safety
130
+ ///
131
+ /// - `format` must be a valid, null-terminated UTF-8 string.
132
+ #[unsafe(no_mangle)]
133
+ pub unsafe extern "C" fn rdx_cypher_schema(format: *const c_char) -> *const c_char {
134
+ let format_str = unsafe { utils::convert_char_ptr_to_string(format) }.unwrap_or_else(|_| "table".to_string());
135
+ let output_format = if format_str == "json" {
136
+ OutputFormat::Json
137
+ } else {
138
+ OutputFormat::Table
139
+ };
140
+
141
+ utils::cstring_raw(&cypher::schema(output_format))
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Structured result types (object-returning query execution)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /// Tag for a structured result cell.
149
+ #[repr(C)]
150
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
151
+ pub enum CCellTag {
152
+ Null = 0,
153
+ Bool = 1,
154
+ Int = 2,
155
+ Str = 3,
156
+ Node = 4,
157
+ List = 5,
158
+ Map = 6,
159
+ }
160
+
161
+ /// Which family of graph node a `Node` cell refers to (selects the Ruby handle class family).
162
+ #[repr(C)]
163
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
164
+ pub enum CNodeCategory {
165
+ Declaration = 0,
166
+ Definition = 1,
167
+ Document = 2,
168
+ }
169
+
170
+ /// `Node` cell payload: which handle family to build, the kind value, and the entity id.
171
+ #[repr(C)]
172
+ #[derive(Debug, Clone, Copy)]
173
+ pub struct CNode {
174
+ /// Which handle family to build.
175
+ pub category: CNodeCategory,
176
+ /// The `CDeclarationKind`/`DefinitionKind` value (ignored for documents).
177
+ pub kind: u32,
178
+ /// The entity id to build the handle from.
179
+ pub id: u64,
180
+ }
181
+
182
+ /// `List` cell payload: a heap array of nested cells (freed by `rdx_rows_iter_free`).
183
+ #[repr(C)]
184
+ #[derive(Debug, Clone, Copy)]
185
+ pub struct CList {
186
+ pub items: *mut CCell,
187
+ pub len: usize,
188
+ }
189
+
190
+ /// `Map` cell payload: parallel heap arrays of owned key strings and their value cells, in order
191
+ /// (both `len` long, freed by `rdx_rows_iter_free`). Stored as separate arrays rather than an
192
+ /// entry struct so no `CCell` is embedded by value (keeping the generated header well-ordered).
193
+ #[repr(C)]
194
+ #[derive(Debug, Clone, Copy)]
195
+ pub struct CMap {
196
+ pub keys: *mut *const c_char,
197
+ pub values: *mut CCell,
198
+ pub len: usize,
199
+ }
200
+
201
+ /// Payload of a `CCell`. The active field is selected by the cell's `tag`; reading any other field
202
+ /// is undefined. `Null` carries no payload.
203
+ #[repr(C)]
204
+ #[derive(Clone, Copy)]
205
+ pub union CCellPayload {
206
+ /// `Bool`.
207
+ pub bool_val: bool,
208
+ /// `Int`.
209
+ pub int_val: i64,
210
+ /// `Str`: owned C string (freed by `rdx_rows_iter_free`).
211
+ pub str_val: *const c_char,
212
+ /// `Node`.
213
+ pub node: CNode,
214
+ /// `List`.
215
+ pub list: CList,
216
+ /// `Map`.
217
+ pub map: CMap,
218
+ }
219
+
220
+ /// A single structured result value: a `tag` discriminant plus a `payload` union whose active
221
+ /// field the tag selects.
222
+ #[repr(C)]
223
+ pub struct CCell {
224
+ pub tag: CCellTag,
225
+ pub payload: CCellPayload,
226
+ }
227
+
228
+ impl CCell {
229
+ fn new(tag: CCellTag, payload: CCellPayload) -> Self {
230
+ Self { tag, payload }
231
+ }
232
+
233
+ fn null() -> Self {
234
+ Self {
235
+ tag: CCellTag::Null,
236
+ payload: CCellPayload { int_val: 0 },
237
+ }
238
+ }
239
+ }
240
+
241
+ /// One row of structured cells.
242
+ #[repr(C)]
243
+ #[derive(Clone, Copy)]
244
+ pub struct CResultRow {
245
+ pub cells: *mut CCell,
246
+ pub len: usize,
247
+ }
248
+
249
+ /// The outcome of one `rdx_rows_iter_next` call.
250
+ #[repr(C)]
251
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
252
+ pub enum CRowsNextStatus {
253
+ /// `out` holds the next row.
254
+ Row,
255
+ /// The cursor reached the end of the result set.
256
+ Done,
257
+ /// The graph no longer holds a node that the row returned, so the row cannot be built.
258
+ /// `rdx_rows_iter_error` names the node.
259
+ MissingNode,
260
+ }
261
+
262
+ /// A cursor over an executed result set's rows. It converts one row per `rdx_rows_iter_next` call,
263
+ /// so a caller can walk a large result set without a copy of every cell in memory at once. Opaque
264
+ /// from the C side — use the `rdx_rows_iter_*` methods to work with it.
265
+ pub struct CRowsIter {
266
+ /// Borrowed from the caller, which must keep it alive for the whole life of the cursor.
267
+ result_set: *const CResultSet,
268
+ graph: GraphPointer,
269
+ columns: Box<[*const c_char]>,
270
+ /// Cells of the row that the last `rdx_rows_iter_next` call produced.
271
+ current: Vec<CCell>,
272
+ /// Name of the node that the last `rdx_rows_iter_next` call could not resolve.
273
+ error: Option<CString>,
274
+ index: usize,
275
+ }
276
+
277
+ /// An executed query's result set: the column names and rows that `rdx_query_execute` produced.
278
+ /// Opaque from the C side — use `rdx_result_set_*` methods to work with it.
279
+ pub struct CResultSet(cypher::ResultSet);
280
+
281
+ /// The result of executing a parsed query against a graph.
282
+ #[repr(C)]
283
+ pub struct CExecuteResult {
284
+ /// Non-null on success; free with `rdx_result_set_free`.
285
+ pub result_set: *mut CResultSet,
286
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
287
+ pub error: *const c_char,
288
+ /// Which kind of failure `error` describes; `None` when `error` is null.
289
+ pub error_kind: CQueryErrorKind,
290
+ }
291
+
292
+ /// Converts a `CypherValue` into a `CCell`, resolving node identity to a handle-buildable category +
293
+ /// kind + id.
294
+ ///
295
+ /// # Errors
296
+ ///
297
+ /// Returns the node's display name when the graph no longer holds a node that the result set
298
+ /// returned, or when its id cannot be decoded. The caller must treat the whole row as stale,
299
+ /// because a fallback would silently change the column's type from a handle to a string. Cells that
300
+ /// this function already built are freed before it returns.
301
+ fn build_cell(graph: &Graph, value: &CypherValue) -> Result<CCell, String> {
302
+ match value {
303
+ CypherValue::Null => Ok(CCell::null()),
304
+ CypherValue::Bool(b) => Ok(CCell::new(CCellTag::Bool, CCellPayload { bool_val: *b })),
305
+ CypherValue::Int(i) => Ok(CCell::new(CCellTag::Int, CCellPayload { int_val: *i })),
306
+ CypherValue::Str(s) => Ok(CCell::new(
307
+ CCellTag::Str,
308
+ CCellPayload {
309
+ str_val: utils::cstring_raw(s),
310
+ },
311
+ )),
312
+ CypherValue::List(items) => {
313
+ let mut cells: Vec<CCell> = Vec::with_capacity(items.len());
314
+
315
+ for item in items {
316
+ match build_cell(graph, item) {
317
+ Ok(cell) => cells.push(cell),
318
+ Err(node) => {
319
+ // SAFETY: `cells` holds only what this loop built, and nothing else owns it.
320
+ unsafe { free_cells(&cells) };
321
+ return Err(node);
322
+ }
323
+ }
324
+ }
325
+
326
+ let len = cells.len();
327
+ let items = if cells.is_empty() {
328
+ ptr::null_mut()
329
+ } else {
330
+ Box::into_raw(cells.into_boxed_slice()).cast::<CCell>()
331
+ };
332
+ Ok(CCell::new(
333
+ CCellTag::List,
334
+ CCellPayload {
335
+ list: CList { items, len },
336
+ },
337
+ ))
338
+ }
339
+ CypherValue::Map(pairs) => {
340
+ let len = pairs.len();
341
+ let mut keys: Vec<*const c_char> = Vec::with_capacity(len);
342
+ let mut values: Vec<CCell> = Vec::with_capacity(len);
343
+
344
+ for (key, val) in pairs {
345
+ match build_cell(graph, val) {
346
+ Ok(cell) => {
347
+ keys.push(utils::cstring_raw(key));
348
+ values.push(cell);
349
+ }
350
+ Err(node) => {
351
+ // SAFETY: both vectors hold only what this loop built.
352
+ unsafe { free_cells(&values) };
353
+ for key in keys {
354
+ let _ = unsafe { CString::from_raw(key.cast_mut()) };
355
+ }
356
+ return Err(node);
357
+ }
358
+ }
359
+ }
360
+
361
+ let (keys, values) = if len == 0 {
362
+ (ptr::null_mut(), ptr::null_mut())
363
+ } else {
364
+ (
365
+ Box::into_raw(keys.into_boxed_slice()).cast::<*const c_char>(),
366
+ Box::into_raw(values.into_boxed_slice()).cast::<CCell>(),
367
+ )
368
+ };
369
+ Ok(CCell::new(
370
+ CCellTag::Map,
371
+ CCellPayload {
372
+ map: CMap { keys, values, len },
373
+ },
374
+ ))
375
+ }
376
+ CypherValue::Node { id, name, .. } => build_node_cell(graph, id).ok_or_else(|| name.clone()),
377
+ }
378
+ }
379
+
380
+ /// Builds a `Node` cell by decoding the opaque node id and looking up its kind in the graph.
381
+ fn build_node_cell(graph: &Graph, encoded_id: &str) -> Option<CCell> {
382
+ match NodeRef::decode(encoded_id)? {
383
+ NodeRef::Declaration(id) => {
384
+ let kind = CDeclaration::kind_from_declaration(graph.declarations().get(&id)?);
385
+ Some(CCell::new(
386
+ CCellTag::Node,
387
+ CCellPayload {
388
+ node: CNode {
389
+ category: CNodeCategory::Declaration,
390
+ kind: kind as u32,
391
+ id: *id,
392
+ },
393
+ },
394
+ ))
395
+ }
396
+ NodeRef::Definition(id) => {
397
+ let kind = map_definition_to_kind(graph.definitions().get(&id)?);
398
+ Some(CCell::new(
399
+ CCellTag::Node,
400
+ CCellPayload {
401
+ node: CNode {
402
+ category: CNodeCategory::Definition,
403
+ kind: kind as u32,
404
+ id: *id,
405
+ },
406
+ },
407
+ ))
408
+ }
409
+ NodeRef::Document(id) => graph.documents().contains_key(&id).then(|| {
410
+ CCell::new(
411
+ CCellTag::Node,
412
+ CCellPayload {
413
+ node: CNode {
414
+ category: CNodeCategory::Document,
415
+ kind: 0,
416
+ id: *id,
417
+ },
418
+ },
419
+ )
420
+ }),
421
+ }
422
+ }
423
+
424
+ /// Executes a previously parsed query against the graph and returns its result set. Format the
425
+ /// result set with `rdx_result_set_format`, or read it as structured rows with
426
+ /// `rdx_result_set_rows`; either way the query runs only once.
427
+ ///
428
+ /// # Safety
429
+ ///
430
+ /// - `query` must be a valid pointer returned by `rdx_cypher_parse`.
431
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
432
+ #[unsafe(no_mangle)]
433
+ pub unsafe extern "C" fn rdx_query_execute(query: *const c_void, pointer: GraphPointer) -> CExecuteResult {
434
+ if query.is_null() {
435
+ return CExecuteResult {
436
+ result_set: ptr::null_mut(),
437
+ error: utils::cstring_raw("query is null"),
438
+ error_kind: CQueryErrorKind::Argument,
439
+ };
440
+ }
441
+
442
+ let parsed = unsafe { &*query.cast::<cypher::Query>() };
443
+
444
+ with_graph(pointer, |graph| match cypher::execute(graph, parsed) {
445
+ Ok(result_set) => CExecuteResult {
446
+ result_set: Box::into_raw(Box::new(CResultSet(result_set))),
447
+ error: ptr::null(),
448
+ error_kind: CQueryErrorKind::None,
449
+ },
450
+ Err(error) => CExecuteResult {
451
+ result_set: ptr::null_mut(),
452
+ error: utils::cstring_raw(&error.to_string()),
453
+ error_kind: CQueryErrorKind::of(&error),
454
+ },
455
+ })
456
+ }
457
+
458
+ /// Formats an executed result set as `format` (`"table"` or `"json"`) without running the query
459
+ /// again. A non-null `error` always describes an invalid argument.
460
+ ///
461
+ /// # Safety
462
+ ///
463
+ /// - `result_set` must be a valid pointer returned by `rdx_query_execute`, or null.
464
+ /// - `format` must be a valid, null-terminated UTF-8 string.
465
+ #[unsafe(no_mangle)]
466
+ pub unsafe extern "C" fn rdx_result_set_format(result_set: *const CResultSet, format: *const c_char) -> CQueryResult {
467
+ if result_set.is_null() {
468
+ return CQueryResult::error("result set is null");
469
+ }
470
+
471
+ let Ok(format_str) = (unsafe { utils::convert_char_ptr_to_string(format) }) else {
472
+ return CQueryResult::error("format is not valid UTF-8");
473
+ };
474
+
475
+ let output_format = match format_str.as_str() {
476
+ "table" => OutputFormat::Table,
477
+ "json" => OutputFormat::Json,
478
+ other => {
479
+ return CQueryResult::error(&format!("unknown query format `{other}` (expected `table` or `json`)"));
480
+ }
481
+ };
482
+
483
+ let result_set = unsafe { &*result_set };
484
+ CQueryResult::success(&cypher::render(&result_set.0, output_format))
485
+ }
486
+
487
+ /// Returns the number of columns in an executed result set.
488
+ ///
489
+ /// # Safety
490
+ ///
491
+ /// - `result_set` must be a valid pointer returned by `rdx_query_execute`, or null.
492
+ #[unsafe(no_mangle)]
493
+ pub unsafe extern "C" fn rdx_result_set_column_count(result_set: *const CResultSet) -> usize {
494
+ if result_set.is_null() {
495
+ return 0;
496
+ }
497
+
498
+ unsafe { &*result_set }.0.columns.len()
499
+ }
500
+
501
+ /// Returns the name of the column at `index`, or null when `index` is out of range. The caller must
502
+ /// free the returned string with `free_c_string`.
503
+ ///
504
+ /// # Safety
505
+ ///
506
+ /// - `result_set` must be a valid pointer returned by `rdx_query_execute`, or null.
507
+ #[unsafe(no_mangle)]
508
+ pub unsafe extern "C" fn rdx_result_set_column(result_set: *const CResultSet, index: usize) -> *const c_char {
509
+ if result_set.is_null() {
510
+ return ptr::null();
511
+ }
512
+
513
+ match unsafe { &*result_set }.0.columns.get(index) {
514
+ Some(name) => utils::cstring_raw(name),
515
+ None => ptr::null(),
516
+ }
517
+ }
518
+
519
+ /// Returns the number of rows in an executed result set.
520
+ ///
521
+ /// # Safety
522
+ ///
523
+ /// - `result_set` must be a valid pointer returned by `rdx_query_execute`, or null.
524
+ #[unsafe(no_mangle)]
525
+ pub unsafe extern "C" fn rdx_result_set_row_count(result_set: *const CResultSet) -> usize {
526
+ if result_set.is_null() {
527
+ return 0;
528
+ }
529
+
530
+ unsafe { &*result_set }.0.rows.len()
531
+ }
532
+
533
+ /// Opens a cursor over the rows of an executed result set, so callers can build their own
534
+ /// value/handle objects instead of formatted text. The cursor converts a row only when
535
+ /// `rdx_rows_iter_next` asks for it. Returns null when `result_set` is null.
536
+ ///
537
+ /// # Safety
538
+ ///
539
+ /// - `result_set` must be a valid pointer returned by `rdx_query_execute`, or null. It must stay
540
+ /// alive until `rdx_rows_iter_free` releases the cursor.
541
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate. It must stay valid
542
+ /// for the same span.
543
+ #[unsafe(no_mangle)]
544
+ pub unsafe extern "C" fn rdx_result_set_rows(result_set: *const CResultSet, pointer: GraphPointer) -> *mut CRowsIter {
545
+ if result_set.is_null() {
546
+ return ptr::null_mut();
547
+ }
548
+
549
+ let columns: Box<[*const c_char]> = unsafe { &*result_set }
550
+ .0
551
+ .columns
552
+ .iter()
553
+ .map(|name| utils::cstring_raw(name))
554
+ .collect();
555
+
556
+ Box::into_raw(Box::new(CRowsIter {
557
+ result_set,
558
+ graph: pointer,
559
+ columns,
560
+ current: Vec::new(),
561
+ error: None,
562
+ index: 0,
563
+ }))
564
+ }
565
+
566
+ /// Frees a result set previously returned by `rdx_query_execute`.
567
+ ///
568
+ /// # Safety
569
+ ///
570
+ /// - `result_set` must be a pointer returned by `rdx_query_execute`, or null. It must not be used
571
+ /// after.
572
+ #[unsafe(no_mangle)]
573
+ pub unsafe extern "C" fn rdx_result_set_free(result_set: *mut CResultSet) {
574
+ if result_set.is_null() {
575
+ return;
576
+ }
577
+
578
+ let _ = unsafe { Box::from_raw(result_set) };
579
+ }
580
+
581
+ /// Returns the number of columns in the result set.
582
+ ///
583
+ /// # Safety
584
+ ///
585
+ /// - `iter` must be a valid pointer returned by `rdx_result_set_rows`, or null.
586
+ #[unsafe(no_mangle)]
587
+ pub unsafe extern "C" fn rdx_rows_iter_column_count(iter: *const CRowsIter) -> usize {
588
+ if iter.is_null() {
589
+ return 0;
590
+ }
591
+ let iter = unsafe { &*iter };
592
+ iter.columns.len()
593
+ }
594
+
595
+ /// Returns a pointer to the array of column name C strings. The array has
596
+ /// `rdx_rows_iter_column_count(iter)` entries and is valid for the lifetime of the iterator.
597
+ ///
598
+ /// # Safety
599
+ ///
600
+ /// - `iter` must be a valid pointer returned by `rdx_result_set_rows`, or null.
601
+ #[unsafe(no_mangle)]
602
+ pub unsafe extern "C" fn rdx_rows_iter_columns(iter: *const CRowsIter) -> *const *const c_char {
603
+ if iter.is_null() {
604
+ return ptr::null();
605
+ }
606
+ let iter = unsafe { &*iter };
607
+ iter.columns.as_ptr()
608
+ }
609
+
610
+ /// Returns the number of rows in the result set.
611
+ ///
612
+ /// # Safety
613
+ ///
614
+ /// - `iter` must be a valid pointer returned by `rdx_result_set_rows`, or null.
615
+ #[unsafe(no_mangle)]
616
+ pub unsafe extern "C" fn rdx_rows_iter_len(iter: *const CRowsIter) -> usize {
617
+ if iter.is_null() {
618
+ return 0;
619
+ }
620
+ let iter = unsafe { &*iter };
621
+ unsafe { &*iter.result_set }.0.rows.len()
622
+ }
623
+
624
+ /// Converts the next row and copies a view of it into `out`. The cells belong to the cursor, so the
625
+ /// copied `CResultRow` stays valid only until the next `rdx_rows_iter_next` call or
626
+ /// `rdx_rows_iter_free`, whichever comes first. Read the row's values before calling either.
627
+ ///
628
+ /// Returns `MissingNode` when the graph no longer holds a node that the row returned. That happens
629
+ /// when the graph changed after the query ran. The cursor keeps the node's name for
630
+ /// `rdx_rows_iter_error`, and the caller should stop the walk.
631
+ ///
632
+ /// The graph read lock is taken for the conversion of one row and released before this function
633
+ /// returns, so a caller may run arbitrary code, including code that writes to the graph, between
634
+ /// two calls.
635
+ ///
636
+ /// # Safety
637
+ ///
638
+ /// - `iter` must be a valid pointer returned by `rdx_result_set_rows`, or null.
639
+ /// - `out` must be a valid, writable pointer, or null.
640
+ #[unsafe(no_mangle)]
641
+ pub unsafe extern "C" fn rdx_rows_iter_next(iter: *mut CRowsIter, out: *mut CResultRow) -> CRowsNextStatus {
642
+ if iter.is_null() || out.is_null() {
643
+ return CRowsNextStatus::Done;
644
+ }
645
+
646
+ let it = unsafe { &mut *iter };
647
+
648
+ // The previous row is out of scope for the caller now, so release its cells before the next one.
649
+ unsafe { free_cells(&it.current) };
650
+ it.current.clear();
651
+ it.error = None;
652
+
653
+ let result_set = unsafe { &*it.result_set };
654
+ let Some(row) = result_set.0.rows.get(it.index) else {
655
+ return CRowsNextStatus::Done;
656
+ };
657
+ it.index += 1;
658
+
659
+ let built = with_graph(it.graph, |graph| {
660
+ let mut cells: Vec<CCell> = Vec::with_capacity(row.len());
661
+
662
+ for value in row {
663
+ match build_cell(graph, value) {
664
+ Ok(cell) => cells.push(cell),
665
+ Err(node) => {
666
+ // SAFETY: `cells` holds only what this loop built, and nothing else owns it.
667
+ unsafe { free_cells(&cells) };
668
+ return Err(node);
669
+ }
670
+ }
671
+ }
672
+
673
+ Ok(cells)
674
+ });
675
+
676
+ match built {
677
+ Ok(cells) => {
678
+ it.current = cells;
679
+ unsafe {
680
+ *out = CResultRow {
681
+ cells: it.current.as_mut_ptr(),
682
+ len: it.current.len(),
683
+ };
684
+ }
685
+ CRowsNextStatus::Row
686
+ }
687
+ Err(node) => {
688
+ it.error = CString::new(node).ok();
689
+ CRowsNextStatus::MissingNode
690
+ }
691
+ }
692
+ }
693
+
694
+ /// Returns the name of the node that the last `rdx_rows_iter_next` call could not resolve, or null
695
+ /// when it resolved every node. The string belongs to the cursor, so it stays valid only until the
696
+ /// next `rdx_rows_iter_next` call or `rdx_rows_iter_free`.
697
+ ///
698
+ /// # Safety
699
+ ///
700
+ /// - `iter` must be a valid pointer returned by `rdx_result_set_rows`, or null.
701
+ #[unsafe(no_mangle)]
702
+ pub unsafe extern "C" fn rdx_rows_iter_error(iter: *const CRowsIter) -> *const c_char {
703
+ if iter.is_null() {
704
+ return ptr::null();
705
+ }
706
+
707
+ match unsafe { &*iter }.error.as_ref() {
708
+ Some(name) => name.as_ptr(),
709
+ None => ptr::null(),
710
+ }
711
+ }
712
+
713
+ /// Recursively frees a `CCell`'s owned allocations (its string, or its nested list cells).
714
+ unsafe fn free_cell(cell: &CCell) {
715
+ match cell.tag {
716
+ // SAFETY: the tag selects the active union field.
717
+ CCellTag::Str => {
718
+ let str_val = unsafe { cell.payload.str_val };
719
+ if !str_val.is_null() {
720
+ let _ = unsafe { CString::from_raw(str_val.cast_mut()) };
721
+ }
722
+ }
723
+ // SAFETY: the tag selects the active union field.
724
+ CCellTag::List => {
725
+ let list = unsafe { cell.payload.list };
726
+ if !list.items.is_null() && list.len > 0 {
727
+ let slice = unsafe { Box::from_raw(ptr::slice_from_raw_parts_mut(list.items, list.len)) };
728
+ for nested in &slice {
729
+ unsafe { free_cell(nested) };
730
+ }
731
+ }
732
+ }
733
+ // SAFETY: the tag selects the active union field.
734
+ CCellTag::Map => {
735
+ let map = unsafe { cell.payload.map };
736
+ if map.len > 0 {
737
+ if !map.keys.is_null() {
738
+ let keys = unsafe { Box::from_raw(ptr::slice_from_raw_parts_mut(map.keys, map.len)) };
739
+ for key in &keys {
740
+ if !key.is_null() {
741
+ let _ = unsafe { CString::from_raw(key.cast_mut()) };
742
+ }
743
+ }
744
+ }
745
+ if !map.values.is_null() {
746
+ let values = unsafe { Box::from_raw(ptr::slice_from_raw_parts_mut(map.values, map.len)) };
747
+ for nested in &values {
748
+ unsafe { free_cell(nested) };
749
+ }
750
+ }
751
+ }
752
+ }
753
+ _ => {}
754
+ }
755
+ }
756
+
757
+ /// Frees every cell of one row.
758
+ ///
759
+ /// # Safety
760
+ ///
761
+ /// - `cells` must come from `build_cell`, and nothing else may own their allocations.
762
+ unsafe fn free_cells(cells: &[CCell]) {
763
+ for cell in cells {
764
+ unsafe { free_cell(cell) };
765
+ }
766
+ }
767
+
768
+ /// Frees a `CRowsIter` previously returned by `rdx_result_set_rows`, including its column strings
769
+ /// and the cells of the row it converted last.
770
+ ///
771
+ /// # Safety
772
+ ///
773
+ /// - `iter` must be a pointer returned by `rdx_result_set_rows`, or null. It must not be used after.
774
+ #[unsafe(no_mangle)]
775
+ pub unsafe extern "C" fn rdx_rows_iter_free(iter: *mut CRowsIter) {
776
+ if iter.is_null() {
777
+ return;
778
+ }
779
+
780
+ let it = unsafe { Box::from_raw(iter) };
781
+
782
+ // The cursor owns the cells of the last row it produced. The `Vec` and the boxed slice of
783
+ // column pointers drop with `it`; their contents do not.
784
+ unsafe { free_cells(&it.current) };
785
+
786
+ for &col in &it.columns {
787
+ if !col.is_null() {
788
+ let _ = unsafe { CString::from_raw(col.cast_mut()) };
789
+ }
790
+ }
791
+ }