rubydex 0.3.0 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/diagnostic.c +75 -1
  8. data/ext/rubydex/diagnostic.h +2 -0
  9. data/ext/rubydex/graph.c +13 -45
  10. data/ext/rubydex/graph.h +6 -0
  11. data/ext/rubydex/query.c +398 -16
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +11 -4
  14. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  15. data/lib/rubydex/cli/command/console.rb +55 -0
  16. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  17. data/lib/rubydex/cli/command/lint.rb +202 -0
  18. data/lib/rubydex/cli/command/mcp.rb +30 -0
  19. data/lib/rubydex/cli/command/query.rb +70 -0
  20. data/lib/rubydex/cli/command/skill.rb +69 -0
  21. data/lib/rubydex/cli/command.rb +168 -0
  22. data/lib/rubydex/cli.rb +93 -0
  23. data/lib/rubydex/config.rb +59 -0
  24. data/lib/rubydex/diagnostic.rb +12 -3
  25. data/lib/rubydex/errors.rb +42 -1
  26. data/lib/rubydex/graph.rb +10 -3
  27. data/lib/rubydex/linter/custom_rule.rb +97 -0
  28. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  29. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  30. data/lib/rubydex/linter/rule_loader.rb +36 -0
  31. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  32. data/lib/rubydex/linter/runner.rb +56 -0
  33. data/lib/rubydex/linter.rb +19 -0
  34. data/lib/rubydex/location.rb +3 -0
  35. data/lib/rubydex/mcp_server.rb +1 -2
  36. data/lib/rubydex/related_information.rb +17 -0
  37. data/lib/rubydex/rule.rb +33 -0
  38. data/lib/rubydex/severity.rb +70 -0
  39. data/lib/rubydex/skill.rb +88 -0
  40. data/lib/rubydex/skill_registry.rb +62 -0
  41. data/lib/rubydex/version.rb +1 -1
  42. data/lib/rubydex.rb +6 -0
  43. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  44. data/rbi/rubydex.rbi +558 -17
  45. data/rust/Cargo.lock +2 -2
  46. data/rust/rubydex/Cargo.toml +1 -1
  47. data/rust/rubydex/benches/graph_memory.rs +3 -5
  48. data/rust/rubydex/src/config.rs +538 -157
  49. data/rust/rubydex/src/diagnostic.rs +66 -40
  50. data/rust/rubydex/src/errors.rs +0 -1
  51. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  52. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  53. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  54. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  55. data/rust/rubydex/src/lib.rs +1 -0
  56. data/rust/rubydex/src/listing.rs +26 -1
  57. data/rust/rubydex/src/main.rs +7 -4
  58. data/rust/rubydex/src/model/declaration.rs +302 -219
  59. data/rust/rubydex/src/model/graph.rs +27 -40
  60. data/rust/rubydex/src/model/name.rs +58 -17
  61. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  62. data/rust/rubydex/src/path_helpers.rs +77 -0
  63. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  64. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  65. data/rust/rubydex/src/query/cypher.rs +8 -11
  66. data/rust/rubydex/src/query.rs +123 -43
  67. data/rust/rubydex/src/resolution.rs +139 -187
  68. data/rust/rubydex/src/resolution_tests.rs +247 -19
  69. data/rust/rubydex/src/test_utils/context.rs +2 -1
  70. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  71. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  72. data/rust/rubydex/tests/cli.rs +4 -4
  73. data/rust/rubydex-sys/src/config_api.rs +205 -0
  74. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  75. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  76. data/rust/rubydex-sys/src/graph_api.rs +13 -194
  77. data/rust/rubydex-sys/src/lib.rs +2 -0
  78. data/rust/rubydex-sys/src/name_api.rs +2 -6
  79. data/rust/rubydex-sys/src/utils.rs +37 -0
  80. data/skills/send-private-method/SKILL.md +133 -0
  81. metadata +31 -2
@@ -6,6 +6,80 @@
6
6
  * mRubydex = rb_define_module("Rubydex")
7
7
  */
8
8
 
9
+ static VALUE mRubydex;
10
+ static VALUE mRules;
9
11
  VALUE cDiagnostic;
12
+ static VALUE cRule;
13
+ static ID id_default_severity;
10
14
 
11
- void rdxi_initialize_diagnostic(VALUE mRubydex) { cDiagnostic = rb_define_class_under(mRubydex, "Diagnostic", rb_cObject); }
15
+ VALUE rdxi_build_diagnostic_severity_value(VALUE mRubydex, DiagnosticSeverity severity) {
16
+ VALUE mSeverity = rb_const_get(mRubydex, rb_intern("Severity"));
17
+
18
+ switch (severity) {
19
+ case DiagnosticSeverity_Error:
20
+ return rb_const_get(mSeverity, rb_intern("Error"));
21
+ case DiagnosticSeverity_Warning:
22
+ return rb_const_get(mSeverity, rb_intern("Warning"));
23
+ case DiagnosticSeverity_Information:
24
+ return rb_const_get(mSeverity, rb_intern("Information"));
25
+ case DiagnosticSeverity_Hint:
26
+ return rb_const_get(mSeverity, rb_intern("Hint"));
27
+ default:
28
+ rb_raise(rb_eRuntimeError, "Unknown DiagnosticSeverity: %d", severity);
29
+ }
30
+
31
+ return Qnil;
32
+ }
33
+
34
+ /*
35
+ * call-seq:
36
+ * default_severity -> Rubydex::Severity::Base
37
+ *
38
+ * Returns the Rubydex::Severity subclass the diagnostics of this rule get unless the configuration overrides it.
39
+ */
40
+ static VALUE rdxr_generated_rule_default_severity(VALUE self) {
41
+ VALUE severity = rb_attr_get(self, id_default_severity);
42
+
43
+ // Class-level instance variables are not inherited, so a class inheriting from a generated rule reaches this without
44
+ // a severity of its own.
45
+ if (NIL_P(severity)) {
46
+ rb_raise(rb_eRuntimeError, "Rule %s has no default severity", rb_class2name(self));
47
+ }
48
+
49
+ return severity;
50
+ }
51
+
52
+ // Generates a rule class for every rule the graph can report. Severities are resolved here rather than when they are
53
+ // read, which is why Rubydex::Severity is loaded before this extension (see `lib/rubydex.rb`).
54
+ static void define_generated_rules(void) {
55
+ CRuleArray rule_array = rdx_rules();
56
+ VALUE rules = rb_ary_new_capa((long)rule_array.len);
57
+
58
+ for (size_t i = 0; i < rule_array.len; i++) {
59
+ CRule built_in_rule = rule_array.items[i];
60
+ VALUE name = rb_utf8_str_new(built_in_rule.name, (long)built_in_rule.name_length);
61
+ VALUE rule = rb_define_class_under(mRules, StringValueCStr(name), cRule);
62
+ VALUE severity = rdxi_build_diagnostic_severity_value(mRubydex, built_in_rule.default_severity);
63
+
64
+ rb_ivar_set(rule, id_default_severity, severity);
65
+ rb_define_singleton_method(rule, "default_severity", rdxr_generated_rule_default_severity, 0);
66
+ rb_ary_push(rules, rule);
67
+ }
68
+
69
+ rdx_rules_free(rule_array);
70
+ rb_define_const(mRules, "ALL", rb_obj_freeze(rules));
71
+ }
72
+
73
+ VALUE rdxi_rule_class_from_name(const char *name, size_t length) {
74
+ return rb_const_get_at(mRules, rb_intern2(name, (long)length));
75
+ }
76
+
77
+ void rdxi_initialize_diagnostic(VALUE moduleRubydex) {
78
+ mRubydex = moduleRubydex;
79
+ id_default_severity = rb_intern("@default_severity");
80
+
81
+ cDiagnostic = rb_define_class_under(mRubydex, "Diagnostic", rb_cObject);
82
+ cRule = rb_define_class_under(mRubydex, "Rule", rb_cObject);
83
+ mRules = rb_define_module_under(mRubydex, "Rules");
84
+ define_generated_rules();
85
+ }
@@ -7,5 +7,7 @@
7
7
  extern VALUE cDiagnostic;
8
8
 
9
9
  void rdxi_initialize_diagnostic(VALUE mRubydex);
10
+ VALUE rdxi_build_diagnostic_severity_value(VALUE mRubydex, DiagnosticSeverity severity);
11
+ VALUE rdxi_rule_class_from_name(const char *name, size_t length);
10
12
 
11
13
  #endif // RUBYDEX_DIAGNOSTIC_H
data/ext/rubydex/graph.c CHANGED
@@ -1,4 +1,5 @@
1
1
  #include "graph.h"
2
+ #include "config.h"
2
3
  #include "declaration.h"
3
4
  #include "diagnostic.h"
4
5
  #include "document.h"
@@ -492,6 +493,7 @@ static VALUE rdxr_graph_set_encoding(VALUE self, VALUE encoding) {
492
493
  static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE nesting) {
493
494
  Check_Type(const_name, T_STRING);
494
495
  rdxi_check_array_of_strings(nesting);
496
+ const char *const_name_string = StringValueCStr(const_name);
495
497
 
496
498
  // Convert the given file paths into a char** array, so that we can pass to Rust
497
499
  size_t length = RARRAY_LEN(nesting);
@@ -501,7 +503,7 @@ static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE nes
501
503
  TypedData_Get_Struct(self, void *, &graph_type, graph);
502
504
 
503
505
  const CDeclaration *decl =
504
- rdx_graph_resolve_constant(graph, StringValueCStr(const_name), (const char **)converted_file_paths, length);
506
+ rdx_graph_resolve_constant(graph, const_name_string, (const char **)converted_file_paths, length);
505
507
 
506
508
  rdxi_free_str_array(converted_file_paths, length);
507
509
 
@@ -631,7 +633,7 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
631
633
  for (size_t i = 0; i < array->len; i++) {
632
634
  DiagnosticEntry entry = array->items[i];
633
635
  VALUE message = entry.message == NULL ? Qnil : rb_utf8_str_new_cstr(entry.message);
634
- VALUE rule = rb_str_intern(rb_str_new2(entry.rule));
636
+ VALUE rule = rdxi_rule_class_from_name(entry.rule.name, entry.rule.name_length);
635
637
  VALUE location = rdxi_build_location_value(entry.location);
636
638
 
637
639
  VALUE kwargs = rb_hash_new();
@@ -787,6 +789,7 @@ static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE se
787
789
 
788
790
  Check_Type(name, T_STRING);
789
791
  rdxi_check_array_of_strings(nesting);
792
+ const char *name_string = StringValueCStr(name);
790
793
 
791
794
  const char *self_receiver = extract_self_receiver(opts);
792
795
 
@@ -797,7 +800,7 @@ static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE se
797
800
  char **converted_nesting = rdxi_str_array_to_char(nesting, nesting_count);
798
801
 
799
802
  struct CompletionResult result = rdx_graph_complete_method_argument(
800
- graph, StringValueCStr(name), (const char *const *)converted_nesting, nesting_count, self_receiver);
803
+ graph, name_string, (const char *const *)converted_nesting, nesting_count, self_receiver);
801
804
 
802
805
  rdxi_free_str_array(converted_nesting, nesting_count);
803
806
  return completion_result_to_ruby_array(result, self);
@@ -872,52 +875,18 @@ static VALUE rdxr_graph_workspace_path(VALUE self) {
872
875
 
873
876
  /*
874
877
  * call-seq:
875
- * workspace_path=(path) -> void
878
+ * load_config(config) -> void
876
879
  *
877
- * Sets the root directory of the workspace being indexed.
880
+ * Applies a parsed Rubydex::Config to the graph.
878
881
  */
879
- static VALUE rdxr_graph_set_workspace_path(VALUE self, VALUE path) {
880
- Check_Type(path, T_STRING);
881
-
882
- void *graph;
883
- TypedData_Get_Struct(self, void*, &graph_type, graph);
884
-
885
- rdx_graph_set_workspace_path(graph, StringValueCStr(path));
886
- return path;
887
- }
888
-
889
- /*
890
- * call-seq:
891
- * load_config(config_path = nil) -> void
892
- *
893
- * Loads a configuration file for the graph. If `config_path` is nil, loads the default configuration file at
894
- * `workspace_path/rubydex.toml` if it exists. Will raise on malformed files or if an explicit path is given but the
895
- * file does not exist.
896
- */
897
- static VALUE rdxr_graph_load_config(int argc, VALUE *argv, VALUE self) {
898
- VALUE config_path;
899
- rb_scan_args(argc, argv, "01", &config_path);
900
-
882
+ static VALUE rdxr_graph_load_config(VALUE self, VALUE config_obj) {
901
883
  void *graph;
902
884
  TypedData_Get_Struct(self, void *, &graph_type, graph);
903
885
 
904
- const char *config_path_cstr = NULL;
886
+ void *config = rdxi_config_from_object(config_obj);
887
+ rdx_graph_load_config(graph, config);
905
888
 
906
- if (!NIL_P(config_path)) {
907
- Check_Type(config_path, T_STRING);
908
- config_path_cstr = StringValueCStr(config_path);
909
- }
910
-
911
- const char *error = rdx_graph_load_config(graph, config_path_cstr);
912
- if (error == NULL) {
913
- return Qnil;
914
- }
915
-
916
- VALUE message = rb_utf8_str_new_cstr(error);
917
- free_c_string(error);
918
-
919
- VALUE config_error = rb_const_get(mRubydex, rb_intern("ConfigError"));
920
- rb_exc_raise(rb_exc_new_str(config_error, message));
889
+ return Qnil;
921
890
  }
922
891
 
923
892
  /*
@@ -979,7 +948,6 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
979
948
  rb_define_method(cGraph, "exclude_patterns", rdxr_graph_exclude_patterns, 1);
980
949
  rb_define_method(cGraph, "excluded_patterns", rdxr_graph_excluded_patterns, 0);
981
950
  rb_define_method(cGraph, "workspace_path", rdxr_graph_workspace_path, 0);
982
- rb_define_method(cGraph, "workspace_path=", rdxr_graph_set_workspace_path, 1);
983
- rb_define_method(cGraph, "load_config", rdxr_graph_load_config, -1);
951
+ rb_define_method(cGraph, "load_config", rdxr_graph_load_config, 1);
984
952
  rb_define_method(cGraph, "keyword", rdxr_graph_keyword, 1);
985
953
  }
data/ext/rubydex/graph.h CHANGED
@@ -5,6 +5,12 @@
5
5
 
6
6
  extern const rb_data_type_t graph_type;
7
7
 
8
+ static inline void *rdxi_graph_from_object(VALUE graph_obj) {
9
+ void *graph;
10
+ TypedData_Get_Struct(graph_obj, void *, &graph_type, graph);
11
+ return graph;
12
+ }
13
+
8
14
  void rdxi_initialize_graph(VALUE mRubydex);
9
15
 
10
16
  #endif // RUBYDEX_GRAPH_H
data/ext/rubydex/query.c CHANGED
@@ -1,8 +1,43 @@
1
1
  #include "query.h"
2
+ #include "declaration.h"
3
+ #include "definition.h"
4
+ #include "document.h"
2
5
  #include "graph.h"
3
6
  #include "rustbindings.h"
4
7
  #include "utils.h"
5
8
 
9
+ /*
10
+ * RDoc parser workaround for https://github.com/ruby/rdoc/issues/1744:
11
+ * mRubydex = rb_define_module("Rubydex")
12
+ */
13
+
14
+ static VALUE mRubydex;
15
+ static VALUE cQueryResult;
16
+
17
+ // Raises the Ruby error that matches a Cypher failure reported by Rust and releases `message`.
18
+ // Syntax and execution failures get a Rubydex error; everything else is a Ruby argument error.
19
+ NORETURN(static void raise_query_error(const char *message, CQueryErrorKind kind));
20
+
21
+ static void raise_query_error(const char *message, CQueryErrorKind kind) {
22
+ VALUE error_message = rb_utf8_str_new_cstr(message);
23
+ free_c_string(message);
24
+
25
+ VALUE error_class;
26
+ switch (kind) {
27
+ case CQueryErrorKind_Syntax:
28
+ error_class = rb_const_get(mRubydex, rb_intern("QuerySyntaxError"));
29
+ break;
30
+ case CQueryErrorKind_Execution:
31
+ error_class = rb_const_get(mRubydex, rb_intern("QueryExecutionError"));
32
+ break;
33
+ default:
34
+ error_class = rb_eArgError;
35
+ break;
36
+ }
37
+
38
+ rb_exc_raise(rb_exc_new_str(error_class, error_message));
39
+ }
40
+
6
41
  /*
7
42
  * call-seq:
8
43
  * Rubydex::Query.schema(format = :table) -> String
@@ -48,39 +83,364 @@ static const rb_data_type_t query_type = {
48
83
  * Rubydex::Query.parse(query) -> Rubydex::Query
49
84
  *
50
85
  * Parses a Cypher query into an opaque, reusable object without needing a graph. Raises
51
- * ArgumentError on a syntax error, so a query can be validated before building a graph.
86
+ * Rubydex::QuerySyntaxError on a syntax error, so a query can be validated before building a graph.
52
87
  */
53
88
  static VALUE rdxr_query_parse(VALUE klass, VALUE query) {
54
89
  Check_Type(query, T_STRING);
55
90
 
56
91
  struct CParseResult result = rdx_cypher_parse(StringValueCStr(query));
57
92
  if (result.error != NULL) {
58
- VALUE message = rb_utf8_str_new_cstr(result.error);
59
- free_c_string(result.error);
60
- rb_raise(rb_eArgError, "%s", StringValueCStr(message));
93
+ raise_query_error(result.error, result.error_kind);
61
94
  }
62
95
 
63
96
  return TypedData_Wrap_Struct(klass, &query_type, result.query);
64
97
  }
65
98
 
99
+ // Backing data for Rubydex::Query::Result: the executed result set plus the graph it came from.
100
+ typedef struct {
101
+ void *result_set; // Result set owned by Rust, released with rdx_result_set_free
102
+ VALUE graph_obj; // Ruby Graph object to keep it alive, since node cells build handles from it
103
+ VALUE rows; // Memoized array of row hashes, nil until `rows` builds it
104
+ } QueryResultData;
105
+
106
+ // Marks the references movable, so that a compaction can relocate them. `query_result_compact`
107
+ // then writes their new locations back into the struct.
108
+ static void query_result_mark(void *ptr) {
109
+ if (ptr) {
110
+ QueryResultData *data = (QueryResultData *)ptr;
111
+ rb_gc_mark_movable(data->graph_obj);
112
+ rb_gc_mark_movable(data->rows);
113
+ }
114
+ }
115
+
116
+ static void query_result_compact(void *ptr) {
117
+ if (ptr) {
118
+ QueryResultData *data = (QueryResultData *)ptr;
119
+ data->graph_obj = rb_gc_location(data->graph_obj);
120
+ data->rows = rb_gc_location(data->rows);
121
+ }
122
+ }
123
+
124
+ static void query_result_free(void *ptr) {
125
+ if (ptr) {
126
+ QueryResultData *data = (QueryResultData *)ptr;
127
+ rdx_result_set_free(data->result_set);
128
+ xfree(data);
129
+ }
130
+ }
131
+
132
+ static const rb_data_type_t query_result_type = {
133
+ .wrap_struct_name = "Rubydex::Query::Result",
134
+ .function = {
135
+ .dmark = query_result_mark,
136
+ .dfree = query_result_free,
137
+ .dsize = NULL,
138
+ .dcompact = query_result_compact,
139
+ },
140
+ .parent = NULL,
141
+ .data = NULL,
142
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
143
+ };
144
+
145
+ static inline QueryResultData *query_result_data(VALUE self) {
146
+ QueryResultData *data;
147
+ TypedData_Get_Struct(self, QueryResultData, &query_result_type, data);
148
+ return data;
149
+ }
150
+
66
151
  /*
67
152
  * call-seq:
68
- * render(graph, format = :table) -> String
153
+ * run(graph) -> Rubydex::Query::Result
69
154
  *
70
- * Runs this parsed query against +graph+ and returns the formatted output. +format+ may be
71
- * +:table+ (default) or +:json+. Raises ArgumentError on an execution or format error.
155
+ * Runs this parsed query against +graph+ exactly once and returns the result set. Read it as Ruby
156
+ * objects with Rubydex::Query::Result#rows, or format it with Rubydex::Query::Result#render. Raises
157
+ * Rubydex::QueryExecutionError when the query fails against the graph.
72
158
  */
73
- static VALUE rdxr_query_render(int argc, VALUE *argv, VALUE self) {
74
- VALUE graph_obj, format;
75
- rb_scan_args(argc, argv, "11", &graph_obj, &format);
76
-
159
+ static VALUE rdxr_query_run(VALUE self, VALUE graph_obj) {
77
160
  void *query;
78
161
  TypedData_Get_Struct(self, void *, &query_type, query);
79
162
 
80
- void *graph;
81
- TypedData_Get_Struct(graph_obj, void *, &graph_type, graph);
163
+ // Wrap first, so the result set has an owner that frees it even if a later step raises.
164
+ QueryResultData *data;
165
+ VALUE result = TypedData_Make_Struct(cQueryResult, QueryResultData, &query_result_type, data);
166
+ data->result_set = NULL;
167
+ data->graph_obj = graph_obj;
168
+ data->rows = Qnil;
169
+
170
+ struct CExecuteResult executed = rdx_query_execute(query, rdxi_graph_from_object(graph_obj));
171
+ if (executed.error != NULL) {
172
+ raise_query_error(executed.error, executed.error_kind);
173
+ }
174
+
175
+ data->result_set = executed.result_set;
176
+
177
+ return result;
178
+ }
179
+
180
+ // Converts a structured result cell into a Ruby value. Node cells become real graph handles
181
+ // (Declaration / Definition / Document) built against `graph_obj`; lists recurse.
182
+ static VALUE cypher_cell_to_value(VALUE graph_obj, const struct CCell *cell) {
183
+ switch (cell->tag) {
184
+ case CCellTag_Null:
185
+ return Qnil;
186
+ case CCellTag_Bool:
187
+ return cell->payload.bool_val ? Qtrue : Qfalse;
188
+ case CCellTag_Int:
189
+ return LL2NUM(cell->payload.int_val);
190
+ case CCellTag_Str:
191
+ return cell->payload.str_val == NULL ? Qnil : rb_utf8_str_new_cstr(cell->payload.str_val);
192
+ case CCellTag_List: {
193
+ VALUE array = rb_ary_new_capa((long)cell->payload.list.len);
194
+ for (size_t i = 0; i < cell->payload.list.len; i++) {
195
+ rb_ary_push(array, cypher_cell_to_value(graph_obj, &cell->payload.list.items[i]));
196
+ }
197
+ return array;
198
+ }
199
+ case CCellTag_Map: {
200
+ VALUE hash = rb_hash_new();
201
+ for (size_t i = 0; i < cell->payload.map.len; i++) {
202
+ const char *raw_key = cell->payload.map.keys[i];
203
+ VALUE key = raw_key == NULL ? Qnil : rb_utf8_str_new_cstr(raw_key);
204
+ rb_hash_aset(hash, key, cypher_cell_to_value(graph_obj, &cell->payload.map.values[i]));
205
+ }
206
+ return hash;
207
+ }
208
+ case CCellTag_Node: {
209
+ VALUE argv[] = {graph_obj, ULL2NUM(cell->payload.node.id)};
210
+ VALUE klass;
211
+ switch (cell->payload.node.category) {
212
+ case CNodeCategory_Declaration:
213
+ klass = rdxi_declaration_class_for_kind((CDeclarationKind)cell->payload.node.kind);
214
+ break;
215
+ case CNodeCategory_Definition:
216
+ klass = rdxi_definition_class_for_kind((DefinitionKind)cell->payload.node.kind);
217
+ break;
218
+ case CNodeCategory_Document:
219
+ default:
220
+ klass = cDocument;
221
+ break;
222
+ }
223
+ return rb_class_new_instance(2, argv, klass);
224
+ }
225
+ default:
226
+ return Qnil;
227
+ }
228
+ }
229
+
230
+ // Builds the Hash keys of one walk: one frozen UTF-8 String per column. The keys are shared by
231
+ // every row of the walk, so a wide result does not allocate a key String per cell. `rb_hash_aset`
232
+ // stores a frozen String key as it is, instead of duplicating and freezing it.
233
+ static VALUE query_row_keys(struct CRowsIter *iter) {
234
+ size_t count = rdx_rows_iter_column_count(iter);
235
+ const char *const *columns = rdx_rows_iter_columns(iter);
236
+ VALUE keys = rb_ary_new_capa((long)count);
237
+
238
+ for (size_t i = 0; i < count; i++) {
239
+ rb_ary_push(keys, rb_str_freeze(rb_utf8_str_new_cstr(columns[i])));
240
+ }
241
+
242
+ return keys;
243
+ }
244
+
245
+ // Converts one row of the cursor into a Hash keyed by the shared Strings in `keys`.
246
+ static VALUE query_row_to_hash(VALUE graph_obj, VALUE keys, const struct CResultRow *row) {
247
+ long column_count = RARRAY_LEN(keys);
248
+ VALUE hash = rb_hash_new_capa(column_count);
249
+
250
+ for (size_t c = 0; c < row->len && (long)c < column_count; c++) {
251
+ rb_hash_aset(hash, RARRAY_AREF(keys, (long)c), cypher_cell_to_value(graph_obj, &row->cells[c]));
252
+ }
253
+
254
+ return hash;
255
+ }
256
+
257
+ // Raises when the graph no longer holds a node that the query returned. Building a string in place
258
+ // of the missing handle would silently change the column's type, so the walk stops instead.
259
+ NORETURN(static void raise_stale_result(struct CRowsIter *iter));
260
+
261
+ static void raise_stale_result(struct CRowsIter *iter) {
262
+ VALUE error_class = rb_const_get(mRubydex, rb_intern("StaleQueryResultError"));
263
+ const char *node = rdx_rows_iter_error(iter);
264
+
265
+ if (node == NULL) {
266
+ rb_raise(error_class, "the graph no longer holds a node that this query returned");
267
+ }
268
+
269
+ rb_raise(error_class, "the graph no longer holds `%s`, a node that this query returned", node);
270
+ }
271
+
272
+ // Body function for rb_ensure in Rubydex::Query::Result#rows — walks the cursor and collects every
273
+ // row. May raise if a node is gone, or if cell conversion (e.g. handle construction) fails; the
274
+ // ensure function frees the cursor regardless.
275
+ static VALUE query_rows_collect(VALUE args) {
276
+ VALUE graph_obj = rb_ary_entry(args, 0);
277
+ struct CRowsIter *iter = (struct CRowsIter *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
278
+
279
+ VALUE keys = query_row_keys(iter);
280
+ VALUE rows = rb_ary_new_capa((long)rdx_rows_iter_len(iter));
281
+
282
+ struct CResultRow row;
283
+ for (;;) {
284
+ switch (rdx_rows_iter_next(iter, &row)) {
285
+ case CRowsNextStatus_Row:
286
+ rb_ary_push(rows, query_row_to_hash(graph_obj, keys, &row));
287
+ break;
288
+ case CRowsNextStatus_MissingNode:
289
+ raise_stale_result(iter);
290
+ default:
291
+ return rows;
292
+ }
293
+ }
294
+ }
295
+
296
+ // Body function for rb_ensure in Rubydex::Query::Result#each — walks the cursor and yields one row
297
+ // at a time, so only one row exists as Ruby objects at any moment. A `break` or an exception in the
298
+ // block leaves through rb_ensure, which frees the cursor.
299
+ static VALUE query_rows_stream(VALUE args) {
300
+ VALUE graph_obj = rb_ary_entry(args, 0);
301
+ struct CRowsIter *iter = (struct CRowsIter *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
302
+
303
+ VALUE keys = query_row_keys(iter);
304
+
305
+ struct CResultRow row;
306
+ for (;;) {
307
+ switch (rdx_rows_iter_next(iter, &row)) {
308
+ case CRowsNextStatus_Row:
309
+ rb_yield(query_row_to_hash(graph_obj, keys, &row));
310
+ break;
311
+ case CRowsNextStatus_MissingNode:
312
+ raise_stale_result(iter);
313
+ default:
314
+ return Qnil;
315
+ }
316
+ }
317
+ }
318
+
319
+ // Ensure function for rb_ensure to always free the cursor.
320
+ static VALUE query_rows_ensure(VALUE args) {
321
+ struct CRowsIter *iter = (struct CRowsIter *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
322
+ rdx_rows_iter_free(iter);
323
+ return Qnil;
324
+ }
325
+
326
+ // Opens a cursor over the result set's rows and runs `body` with it. The cursor is always freed.
327
+ static VALUE query_with_rows(VALUE self, VALUE (*body)(VALUE)) {
328
+ QueryResultData *data = query_result_data(self);
82
329
 
83
- struct CQueryResult result = rdx_query_run(query, graph, rdxi_symbol_or_string_cstr(format, "table"));
330
+ struct CRowsIter *iter = rdx_result_set_rows(data->result_set, rdxi_graph_from_object(data->graph_obj));
331
+ if (iter == NULL) {
332
+ rb_raise(rb_eRuntimeError, "failed to create iterator");
333
+ }
334
+
335
+ VALUE args = rb_ary_new_from_args(2, data->graph_obj, ULL2NUM((uintptr_t)iter));
336
+ return rb_ensure(body, args, query_rows_ensure, args);
337
+ }
338
+
339
+ /*
340
+ * call-seq:
341
+ * rows -> Array[Hash[String, Object]]
342
+ *
343
+ * Returns the rows as Ruby objects: a frozen Array in which each row is a Hash keyed by RETURN
344
+ * column name. Scalar cells become String/Integer/true/false/nil, lists become Arrays, maps become
345
+ * Hashes, and node cells become Declaration / Definition / Document handles. The array is built on
346
+ * the first call and reused afterwards.
347
+ */
348
+ static VALUE rdxr_query_result_rows(VALUE self) {
349
+ if (!NIL_P(query_result_data(self)->rows)) {
350
+ return query_result_data(self)->rows;
351
+ }
352
+
353
+ VALUE rows = rb_ary_freeze(query_with_rows(self, query_rows_collect));
354
+ query_result_data(self)->rows = rows;
355
+
356
+ return rows;
357
+ }
358
+
359
+ /*
360
+ * call-seq:
361
+ * columns -> Array[String]
362
+ *
363
+ * Returns the RETURN column names, in order. The names are known even when the query matched no
364
+ * rows.
365
+ */
366
+ static VALUE rdxr_query_result_columns(VALUE self) {
367
+ QueryResultData *data = query_result_data(self);
368
+
369
+ size_t count = rdx_result_set_column_count(data->result_set);
370
+ VALUE columns = rb_ary_new_capa((long)count);
371
+
372
+ for (size_t i = 0; i < count; i++) {
373
+ rb_ary_push(columns, rdxi_owned_c_string_to_ruby(rdx_result_set_column(data->result_set, i)));
374
+ }
375
+
376
+ return columns;
377
+ }
378
+
379
+ /*
380
+ * call-seq:
381
+ * each { |row| ... } -> self
382
+ * each -> Enumerator
383
+ *
384
+ * Yields every row as a Hash keyed by RETURN column name. Rubydex::Query::Result is Enumerable, so
385
+ * +map+, +select+, and the rest of Enumerable work on the rows.
386
+ *
387
+ * Unless #rows already built the whole array, +each+ converts one row at a time and discards it
388
+ * after the block returns. A large result therefore needs memory for one row, not for all of them,
389
+ * and +first+ or +find+ stops converting as soon as the block breaks.
390
+ */
391
+ static VALUE rdxr_query_result_each(VALUE self) {
392
+ RETURN_ENUMERATOR(self, 0, 0);
393
+
394
+ VALUE rows = query_result_data(self)->rows;
395
+
396
+ if (NIL_P(rows)) {
397
+ query_with_rows(self, query_rows_stream);
398
+ return self;
399
+ }
400
+
401
+ long length = RARRAY_LEN(rows);
402
+
403
+ for (long i = 0; i < length; i++) {
404
+ rb_yield(RARRAY_AREF(rows, i));
405
+ }
406
+
407
+ return self;
408
+ }
409
+
410
+ /*
411
+ * call-seq:
412
+ * size -> Integer
413
+ * length -> Integer
414
+ *
415
+ * Returns the number of rows, without building the row objects.
416
+ */
417
+ static VALUE rdxr_query_result_size(VALUE self) {
418
+ return SIZET2NUM(rdx_result_set_row_count(query_result_data(self)->result_set));
419
+ }
420
+
421
+ /*
422
+ * call-seq:
423
+ * empty? -> bool
424
+ *
425
+ * Returns +true+ when the query matched no rows.
426
+ */
427
+ static VALUE rdxr_query_result_empty_p(VALUE self) {
428
+ return rdx_result_set_row_count(query_result_data(self)->result_set) == 0 ? Qtrue : Qfalse;
429
+ }
430
+
431
+ /*
432
+ * call-seq:
433
+ * render(format = :table) -> String
434
+ *
435
+ * Returns the result set as formatted output. +format+ may be +:table+ (default) or +:json+. The
436
+ * query is not run again. Raises ArgumentError on an unknown format.
437
+ */
438
+ static VALUE rdxr_query_result_render(int argc, VALUE *argv, VALUE self) {
439
+ VALUE format;
440
+ rb_scan_args(argc, argv, "01", &format);
441
+
442
+ QueryResultData *data = query_result_data(self);
443
+ struct CQueryResult result = rdx_result_set_format(data->result_set, rdxi_symbol_or_string_cstr(format, "table"));
84
444
 
85
445
  if (result.error != NULL) {
86
446
  VALUE message = rb_utf8_str_new_cstr(result.error);
@@ -96,10 +456,32 @@ static VALUE rdxr_query_render(int argc, VALUE *argv, VALUE self) {
96
456
  return output;
97
457
  }
98
458
 
99
- void rdxi_initialize_query(VALUE mRubydex) {
459
+ void rdxi_initialize_query(VALUE moduleRubydex) {
460
+ mRubydex = moduleRubydex;
461
+
100
462
  VALUE cQuery = rb_define_class_under(mRubydex, "Query", rb_cObject);
101
463
  rb_undef_alloc_func(cQuery);
102
464
  rb_define_singleton_method(cQuery, "parse", rdxr_query_parse, 1);
103
465
  rb_define_singleton_method(cQuery, "schema", rdxr_cypher_schema, -1);
104
- rb_define_method(cQuery, "render", rdxr_query_render, -1);
466
+ rb_define_method(cQuery, "run", rdxr_query_run, 1);
467
+
468
+ /*
469
+ * The result of running a Rubydex::Query against a graph: the columns and rows produced by one
470
+ * execution. Enumerable over its rows.
471
+ */
472
+ cQueryResult = rb_define_class_under(cQuery, "Result", rb_cObject);
473
+ rb_undef_alloc_func(cQueryResult);
474
+
475
+ // A result can only be obtained from `Query#run`; `new` would create an object with no Rust
476
+ // data behind it.
477
+ rb_undef_method(rb_singleton_class(cQueryResult), "new");
478
+
479
+ rb_include_module(cQueryResult, rb_mEnumerable);
480
+ rb_define_method(cQueryResult, "columns", rdxr_query_result_columns, 0);
481
+ rb_define_method(cQueryResult, "rows", rdxr_query_result_rows, 0);
482
+ rb_define_method(cQueryResult, "each", rdxr_query_result_each, 0);
483
+ rb_define_method(cQueryResult, "size", rdxr_query_result_size, 0);
484
+ rb_define_alias(cQueryResult, "length", "size");
485
+ rb_define_method(cQueryResult, "empty?", rdxr_query_result_empty_p, 0);
486
+ rb_define_method(cQueryResult, "render", rdxr_query_result_render, -1);
105
487
  }
@@ -1,3 +1,4 @@
1
+ #include "config.h"
1
2
  #include "declaration.h"
2
3
  #include "definition.h"
3
4
  #include "diagnostic.h"
@@ -20,6 +21,7 @@ void Init_rubydex(void) {
20
21
  */
21
22
  mRubydex = rb_define_module("Rubydex");
22
23
  rdxi_initialize_graph(mRubydex);
24
+ rdxi_initialize_config(mRubydex);
23
25
  rdxi_initialize_query(mRubydex);
24
26
  rdxi_initialize_declaration(mRubydex);
25
27
  rdxi_initialize_document(mRubydex);