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.
- checksums.yaml +4 -4
- data/README.md +110 -6
- data/THIRD_PARTY_LICENSES.html +271 -2
- data/exe/rdx +2 -73
- data/ext/rubydex/config.c +140 -0
- data/ext/rubydex/config.h +16 -0
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/diagnostic.c +75 -1
- data/ext/rubydex/diagnostic.h +2 -0
- data/ext/rubydex/graph.c +27 -48
- data/ext/rubydex/graph.h +6 -0
- data/ext/rubydex/query.c +487 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +4 -0
- data/ext/rubydex/utils.c +23 -4
- data/ext/rubydex/utils.h +5 -0
- data/lib/ruby_lsp/rubydex/addon.rb +211 -0
- data/lib/rubydex/cli/command/console.rb +55 -0
- data/lib/rubydex/cli/command/lint/explain.rb +74 -0
- data/lib/rubydex/cli/command/lint.rb +202 -0
- data/lib/rubydex/cli/command/mcp.rb +30 -0
- data/lib/rubydex/cli/command/query.rb +70 -0
- data/lib/rubydex/cli/command/skill.rb +69 -0
- data/lib/rubydex/cli/command.rb +168 -0
- data/lib/rubydex/cli.rb +93 -0
- data/lib/rubydex/config.rb +59 -0
- data/lib/rubydex/diagnostic.rb +12 -3
- data/lib/rubydex/errors.rb +42 -1
- data/lib/rubydex/graph.rb +10 -3
- data/lib/rubydex/linter/custom_rule.rb +97 -0
- data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
- data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
- data/lib/rubydex/linter/rule_loader.rb +36 -0
- data/lib/rubydex/linter/rule_test_case.rb +343 -0
- data/lib/rubydex/linter/runner.rb +56 -0
- data/lib/rubydex/linter.rb +19 -0
- data/lib/rubydex/location.rb +3 -0
- data/lib/rubydex/mcp_server.rb +1 -2
- data/lib/rubydex/related_information.rb +17 -0
- data/lib/rubydex/rule.rb +33 -0
- data/lib/rubydex/severity.rb +70 -0
- data/lib/rubydex/skill.rb +88 -0
- data/lib/rubydex/skill_registry.rb +62 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +6 -0
- data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
- data/rbi/rubydex.rbi +578 -15
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +20 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +538 -157
- data/rust/rubydex/src/diagnostic.rs +66 -40
- data/rust/rubydex/src/errors.rs +0 -1
- data/rust/rubydex/src/indexing/local_graph.rs +6 -5
- data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
- data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
- data/rust/rubydex/src/lib.rs +1 -0
- data/rust/rubydex/src/listing.rs +26 -1
- data/rust/rubydex/src/main.rs +9 -128
- data/rust/rubydex/src/model/declaration.rs +301 -229
- 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 +67 -68
- 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 +88 -19
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
- data/rust/rubydex/src/path_helpers.rs +77 -0
- data/rust/rubydex/src/query/cypher/schema.rs +853 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +253 -0
- data/rust/rubydex/src/query/cypher.rs +54 -0
- data/rust/rubydex/src/query.rs +125 -43
- data/rust/rubydex/src/resolution.rs +368 -395
- data/rust/rubydex/src/resolution_tests.rs +504 -78
- data/rust/rubydex/src/test_utils/context.rs +2 -1
- data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
- data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
- data/rust/rubydex/tests/cli.rs +4 -4
- data/rust/rubydex-sys/src/config_api.rs +205 -0
- data/rust/rubydex-sys/src/cypher_api.rs +791 -0
- 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/diagnostic_api.rs +77 -8
- data/rust/rubydex-sys/src/graph_api.rs +31 -68
- data/rust/rubydex-sys/src/lib.rs +2 -0
- data/rust/rubydex-sys/src/name_api.rs +2 -6
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- data/rust/rubydex-sys/src/utils.rs +37 -0
- data/skills/send-private-method/SKILL.md +133 -0
- metadata +37 -2
data/ext/rubydex/query.c
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#include "query.h"
|
|
2
|
+
#include "declaration.h"
|
|
3
|
+
#include "definition.h"
|
|
4
|
+
#include "document.h"
|
|
5
|
+
#include "graph.h"
|
|
6
|
+
#include "rustbindings.h"
|
|
7
|
+
#include "utils.h"
|
|
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
|
+
|
|
41
|
+
/*
|
|
42
|
+
* call-seq:
|
|
43
|
+
* Rubydex::Query.schema(format = :table) -> String
|
|
44
|
+
*
|
|
45
|
+
* Returns a description of the queryable Cypher schema. +format+ may be +:table+ (default) or
|
|
46
|
+
* +:json+. The schema is static, so it does not require a graph.
|
|
47
|
+
*/
|
|
48
|
+
static VALUE rdxr_cypher_schema(int argc, VALUE *argv, VALUE self) {
|
|
49
|
+
VALUE format;
|
|
50
|
+
rb_scan_args(argc, argv, "01", &format);
|
|
51
|
+
|
|
52
|
+
const char *output = rdx_cypher_schema(rdxi_symbol_or_string_cstr(format, "table"));
|
|
53
|
+
VALUE result = output == NULL ? rb_utf8_str_new_cstr("") : rb_utf8_str_new_cstr(output);
|
|
54
|
+
if (output != NULL) {
|
|
55
|
+
free_c_string(output);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Free function for Rubydex::Query: releases the parsed query allocated by Rust.
|
|
62
|
+
static void query_free(void *ptr) {
|
|
63
|
+
if (ptr) {
|
|
64
|
+
rdx_cypher_query_free(ptr);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static const rb_data_type_t query_type = {
|
|
69
|
+
.wrap_struct_name = "Rubydex::Query",
|
|
70
|
+
.function = {
|
|
71
|
+
.dmark = NULL,
|
|
72
|
+
.dfree = query_free,
|
|
73
|
+
.dsize = NULL,
|
|
74
|
+
.dcompact = NULL,
|
|
75
|
+
},
|
|
76
|
+
.parent = NULL,
|
|
77
|
+
.data = NULL,
|
|
78
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* call-seq:
|
|
83
|
+
* Rubydex::Query.parse(query) -> Rubydex::Query
|
|
84
|
+
*
|
|
85
|
+
* Parses a Cypher query into an opaque, reusable object without needing a graph. Raises
|
|
86
|
+
* Rubydex::QuerySyntaxError on a syntax error, so a query can be validated before building a graph.
|
|
87
|
+
*/
|
|
88
|
+
static VALUE rdxr_query_parse(VALUE klass, VALUE query) {
|
|
89
|
+
Check_Type(query, T_STRING);
|
|
90
|
+
|
|
91
|
+
struct CParseResult result = rdx_cypher_parse(StringValueCStr(query));
|
|
92
|
+
if (result.error != NULL) {
|
|
93
|
+
raise_query_error(result.error, result.error_kind);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return TypedData_Wrap_Struct(klass, &query_type, result.query);
|
|
97
|
+
}
|
|
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
|
+
|
|
151
|
+
/*
|
|
152
|
+
* call-seq:
|
|
153
|
+
* run(graph) -> Rubydex::Query::Result
|
|
154
|
+
*
|
|
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.
|
|
158
|
+
*/
|
|
159
|
+
static VALUE rdxr_query_run(VALUE self, VALUE graph_obj) {
|
|
160
|
+
void *query;
|
|
161
|
+
TypedData_Get_Struct(self, void *, &query_type, query);
|
|
162
|
+
|
|
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);
|
|
329
|
+
|
|
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"));
|
|
444
|
+
|
|
445
|
+
if (result.error != NULL) {
|
|
446
|
+
VALUE message = rb_utf8_str_new_cstr(result.error);
|
|
447
|
+
free_c_string(result.error);
|
|
448
|
+
rb_raise(rb_eArgError, "%s", StringValueCStr(message));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
VALUE output = result.output == NULL ? rb_utf8_str_new_cstr("") : rb_utf8_str_new_cstr(result.output);
|
|
452
|
+
if (result.output != NULL) {
|
|
453
|
+
free_c_string(result.output);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return output;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
void rdxi_initialize_query(VALUE moduleRubydex) {
|
|
460
|
+
mRubydex = moduleRubydex;
|
|
461
|
+
|
|
462
|
+
VALUE cQuery = rb_define_class_under(mRubydex, "Query", rb_cObject);
|
|
463
|
+
rb_undef_alloc_func(cQuery);
|
|
464
|
+
rb_define_singleton_method(cQuery, "parse", rdxr_query_parse, 1);
|
|
465
|
+
rb_define_singleton_method(cQuery, "schema", rdxr_cypher_schema, -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);
|
|
487
|
+
}
|
data/ext/rubydex/query.h
ADDED
data/ext/rubydex/reference.c
CHANGED
|
@@ -17,6 +17,8 @@ VALUE cUnresolvedConstantReference;
|
|
|
17
17
|
VALUE cResolvedConstantReference;
|
|
18
18
|
VALUE cMethodReference;
|
|
19
19
|
|
|
20
|
+
static VALUE cDocument;
|
|
21
|
+
|
|
20
22
|
/*
|
|
21
23
|
* call-seq:
|
|
22
24
|
* name -> String
|
|
@@ -28,6 +30,9 @@ static VALUE rdxr_constant_reference_name(VALUE self) {
|
|
|
28
30
|
void *graph = rdxi_graph_from_handle(self, &data);
|
|
29
31
|
|
|
30
32
|
const char *name = rdx_constant_reference_name(graph, data->id);
|
|
33
|
+
if (name == NULL) {
|
|
34
|
+
rb_raise(rb_eRuntimeError, "Constant reference must exist for a valid id");
|
|
35
|
+
}
|
|
31
36
|
return rdxi_owned_c_string_to_ruby(name);
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -42,11 +47,35 @@ static VALUE rdxr_constant_reference_location(VALUE self) {
|
|
|
42
47
|
void *graph = rdxi_graph_from_handle(self, &data);
|
|
43
48
|
|
|
44
49
|
Location *loc = rdx_constant_reference_location(graph, data->id);
|
|
50
|
+
if (loc == NULL) {
|
|
51
|
+
rb_raise(rb_eRuntimeError, "Constant reference must exist for a valid id");
|
|
52
|
+
}
|
|
45
53
|
VALUE location = rdxi_build_location_value(loc);
|
|
46
54
|
rdx_location_free(loc);
|
|
47
55
|
return location;
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
/*
|
|
59
|
+
* call-seq:
|
|
60
|
+
* document -> Rubydex::Document
|
|
61
|
+
*
|
|
62
|
+
* Returns the document this constant reference belongs to.
|
|
63
|
+
*/
|
|
64
|
+
static VALUE rdxr_constant_reference_document(VALUE self) {
|
|
65
|
+
HandleData *data;
|
|
66
|
+
void *graph = rdxi_graph_from_handle(self, &data);
|
|
67
|
+
|
|
68
|
+
const uint64_t *uri_id = rdx_constant_reference_document(graph, data->id);
|
|
69
|
+
if (uri_id == NULL) {
|
|
70
|
+
rb_raise(rb_eRuntimeError, "Constant reference not found");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
VALUE argv[] = {data->graph_obj, ULL2NUM(*uri_id)};
|
|
74
|
+
free_u64(uri_id);
|
|
75
|
+
|
|
76
|
+
return rb_class_new_instance(2, argv, cDocument);
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
/*
|
|
51
80
|
* call-seq:
|
|
52
81
|
* name -> String
|
|
@@ -58,6 +87,9 @@ static VALUE rdxr_method_reference_name(VALUE self) {
|
|
|
58
87
|
void *graph = rdxi_graph_from_handle(self, &data);
|
|
59
88
|
|
|
60
89
|
const char *name = rdx_method_reference_name(graph, data->id);
|
|
90
|
+
if (name == NULL) {
|
|
91
|
+
rb_raise(rb_eRuntimeError, "Method reference must exist for a valid id");
|
|
92
|
+
}
|
|
61
93
|
return rdxi_owned_c_string_to_ruby(name);
|
|
62
94
|
}
|
|
63
95
|
|
|
@@ -72,6 +104,9 @@ static VALUE rdxr_method_reference_location(VALUE self) {
|
|
|
72
104
|
void *graph = rdxi_graph_from_handle(self, &data);
|
|
73
105
|
|
|
74
106
|
Location *loc = rdx_method_reference_location(graph, data->id);
|
|
107
|
+
if (loc == NULL) {
|
|
108
|
+
rb_raise(rb_eRuntimeError, "Method reference must exist for a valid id");
|
|
109
|
+
}
|
|
75
110
|
VALUE location = rdxi_build_location_value(loc);
|
|
76
111
|
rdx_location_free(loc);
|
|
77
112
|
return location;
|
|
@@ -100,6 +135,27 @@ static VALUE rdxr_method_reference_receiver(VALUE self) {
|
|
|
100
135
|
return rb_class_new_instance(2, argv, decl_class);
|
|
101
136
|
}
|
|
102
137
|
|
|
138
|
+
/*
|
|
139
|
+
* call-seq:
|
|
140
|
+
* document -> Rubydex::Document
|
|
141
|
+
*
|
|
142
|
+
* Returns the document this method reference belongs to.
|
|
143
|
+
*/
|
|
144
|
+
static VALUE rdxr_method_reference_document(VALUE self) {
|
|
145
|
+
HandleData *data;
|
|
146
|
+
void *graph = rdxi_graph_from_handle(self, &data);
|
|
147
|
+
|
|
148
|
+
const uint64_t *uri_id = rdx_method_reference_document(graph, data->id);
|
|
149
|
+
if (uri_id == NULL) {
|
|
150
|
+
rb_raise(rb_eRuntimeError, "Method reference not found");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
VALUE argv[] = {data->graph_obj, ULL2NUM(*uri_id)};
|
|
154
|
+
free_u64(uri_id);
|
|
155
|
+
|
|
156
|
+
return rb_class_new_instance(2, argv, cDocument);
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
/*
|
|
104
160
|
* call-seq:
|
|
105
161
|
* declaration -> Rubydex::Declaration
|
|
@@ -123,6 +179,8 @@ static VALUE rdxr_resolved_constant_reference_declaration(VALUE self) {
|
|
|
123
179
|
}
|
|
124
180
|
|
|
125
181
|
void rdxi_initialize_reference(VALUE mRubydex) {
|
|
182
|
+
cDocument = rb_const_get(mRubydex, rb_intern("Document"));
|
|
183
|
+
|
|
126
184
|
cReference = rb_define_class_under(mRubydex, "Reference", rb_cObject);
|
|
127
185
|
rb_define_alloc_func(cReference, rdxr_handle_alloc);
|
|
128
186
|
rb_define_method(cReference, "initialize", rdxr_handle_initialize, 2);
|
|
@@ -132,6 +190,7 @@ void rdxi_initialize_reference(VALUE mRubydex) {
|
|
|
132
190
|
rb_define_alloc_func(cConstantReference, rdxr_handle_alloc);
|
|
133
191
|
rb_define_method(cConstantReference, "initialize", rdxr_handle_initialize, 2);
|
|
134
192
|
rb_define_method(cConstantReference, "location", rdxr_constant_reference_location, 0);
|
|
193
|
+
rb_define_method(cConstantReference, "document", rdxr_constant_reference_document, 0);
|
|
135
194
|
rb_funcall(rb_singleton_class(cConstantReference), rb_intern("private"), 1, ID2SYM(rb_intern("new")));
|
|
136
195
|
|
|
137
196
|
cUnresolvedConstantReference = rb_define_class_under(mRubydex, "UnresolvedConstantReference", cConstantReference);
|
|
@@ -148,4 +207,5 @@ void rdxi_initialize_reference(VALUE mRubydex) {
|
|
|
148
207
|
rb_define_method(cMethodReference, "name", rdxr_method_reference_name, 0);
|
|
149
208
|
rb_define_method(cMethodReference, "location", rdxr_method_reference_location, 0);
|
|
150
209
|
rb_define_method(cMethodReference, "receiver", rdxr_method_reference_receiver, 0);
|
|
210
|
+
rb_define_method(cMethodReference, "document", rdxr_method_reference_document, 0);
|
|
151
211
|
}
|
data/ext/rubydex/rubydex.c
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
#include "config.h"
|
|
1
2
|
#include "declaration.h"
|
|
2
3
|
#include "definition.h"
|
|
3
4
|
#include "diagnostic.h"
|
|
4
5
|
#include "document.h"
|
|
5
6
|
#include "graph.h"
|
|
6
7
|
#include "location.h"
|
|
8
|
+
#include "query.h"
|
|
7
9
|
#include "reference.h"
|
|
8
10
|
#include "signature.h"
|
|
9
11
|
|
|
@@ -19,6 +21,8 @@ void Init_rubydex(void) {
|
|
|
19
21
|
*/
|
|
20
22
|
mRubydex = rb_define_module("Rubydex");
|
|
21
23
|
rdxi_initialize_graph(mRubydex);
|
|
24
|
+
rdxi_initialize_config(mRubydex);
|
|
25
|
+
rdxi_initialize_query(mRubydex);
|
|
22
26
|
rdxi_initialize_declaration(mRubydex);
|
|
23
27
|
rdxi_initialize_document(mRubydex);
|
|
24
28
|
rdxi_initialize_definition(mRubydex);
|
data/ext/rubydex/utils.c
CHANGED
|
@@ -4,16 +4,23 @@
|
|
|
4
4
|
#include "rustbindings.h"
|
|
5
5
|
|
|
6
6
|
// Convert a Ruby array of strings into a double char pointer so that we can pass that to Rust.
|
|
7
|
-
// This copies the data so it must be freed
|
|
7
|
+
// This copies the data so it must be freed.
|
|
8
8
|
char **rdxi_str_array_to_char(VALUE array, size_t length) {
|
|
9
|
+
// Validate every element before allocating so StringValueCStr cannot strand a partially built array.
|
|
10
|
+
for (size_t i = 0; i < length; i++) {
|
|
11
|
+
VALUE item = rb_ary_entry(array, i);
|
|
12
|
+
Check_Type(item, T_STRING);
|
|
13
|
+
(void)StringValueCStr(item);
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
char **converted_array = malloc(length * sizeof(char *));
|
|
10
17
|
|
|
11
18
|
for (size_t i = 0; i < length; i++) {
|
|
12
19
|
VALUE item = rb_ary_entry(array, i);
|
|
13
|
-
|
|
20
|
+
size_t string_length = (size_t)RSTRING_LEN(item);
|
|
14
21
|
|
|
15
|
-
converted_array[i] = malloc(
|
|
16
|
-
|
|
22
|
+
converted_array[i] = malloc(string_length + 1);
|
|
23
|
+
memcpy(converted_array[i], RSTRING_PTR(item), string_length + 1);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
return converted_array;
|
|
@@ -52,6 +59,18 @@ VALUE rdxi_owned_c_string_to_ruby(const char *string) {
|
|
|
52
59
|
return value;
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
// Coerce an optional String, Symbol, or nil to a C string, defaulting to `default_value` for nil.
|
|
63
|
+
const char *rdxi_symbol_or_string_cstr(VALUE value, const char *default_value) {
|
|
64
|
+
if (NIL_P(value)) {
|
|
65
|
+
return default_value;
|
|
66
|
+
}
|
|
67
|
+
if (RB_TYPE_P(value, T_SYMBOL)) {
|
|
68
|
+
value = rb_sym2str(value);
|
|
69
|
+
}
|
|
70
|
+
Check_Type(value, T_STRING);
|
|
71
|
+
return StringValueCStr(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
// Yield body for iterating over declarations
|
|
56
75
|
VALUE rdxi_declarations_yield(VALUE args) {
|
|
57
76
|
VALUE self = rb_ary_entry(args, 0);
|
data/ext/rubydex/utils.h
CHANGED
|
@@ -17,6 +17,11 @@ void rdxi_check_array_of_strings(VALUE array);
|
|
|
17
17
|
// Returns nil when the Rust side returned NULL.
|
|
18
18
|
VALUE rdxi_owned_c_string_to_ruby(const char *string);
|
|
19
19
|
|
|
20
|
+
// Coerce an optional String, Symbol, or nil to a C string, returning `default_value` when the value
|
|
21
|
+
// is nil. A non-nil value must be a String or Symbol, otherwise a `TypeError` is raised. The
|
|
22
|
+
// returned pointer is owned by the Ruby VALUE and is only valid while it stays live.
|
|
23
|
+
const char *rdxi_symbol_or_string_cstr(VALUE value, const char *default_value);
|
|
24
|
+
|
|
20
25
|
// Yield body for iterating over declarations
|
|
21
26
|
VALUE rdxi_declarations_yield(VALUE args);
|
|
22
27
|
|