ruby-kuzu 0.0.3 → 0.2.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
- checksums.yaml.gz.sig +0 -0
- data/History.md +25 -0
- data/README.md +135 -11
- data/ext/kuzu_ext/connection.c +21 -0
- data/ext/kuzu_ext/kuzu_ext.h +4 -2
- data/ext/kuzu_ext/prepared_statement.c +5 -1
- data/ext/kuzu_ext/query_summary.c +1 -1
- data/ext/kuzu_ext/result.c +1 -1
- data/ext/kuzu_ext/types.c +5 -2
- data/lib/kuzu/prepared_statement.rb +2 -0
- data/lib/kuzu/result.rb +60 -3
- data/lib/kuzu.rb +22 -2
- data/spec/kuzu/connection_spec.rb +36 -0
- data/spec/kuzu/database_spec.rb +8 -2
- data/spec/kuzu/result_spec.rb +37 -2
- data/spec/kuzu/types_spec.rb +30 -0
- data/spec/kuzu_spec.rb +30 -2
- data/spec/spec_helper.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffa06875d65096bb5bac251440d1f1ec56d233a7675f86b9e4d22a5039d91020
|
4
|
+
data.tar.gz: 4666b63219b97e6fcb8781995bbb71bbb486a09f8bdbbe763e6f4b225101750a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd651f4b5f1267a5e1725cc692ca3554842d58c4cb71bb63498172de7d6fcef6df3d7855bbdba2aadd2f35a01482e73104ef2a9647bf4a8221b3dba63e821096
|
7
|
+
data.tar.gz: b65eb14735f2d06883cc4392e67e4cb208fb415ac9de62a24d1e686bad95fa7fb0412124ef01908ce587a223bc1bcb37688d8cb4df10564f888acde3192171bd
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/History.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
---
|
4
4
|
|
5
|
+
## v0.2.0 [2025-07-16] Michael Granger <ged@FaerieMUD.org>
|
6
|
+
|
7
|
+
Enhancements:
|
8
|
+
|
9
|
+
- Add support for the SERIAL type.
|
10
|
+
- Add Kuzu.is_database? method
|
11
|
+
- Flesh out documentation
|
12
|
+
|
13
|
+
Bugfixes:
|
14
|
+
|
15
|
+
- Fixups for Linux and Kuzu 0.11
|
16
|
+
- Fix storage version check (0.11+)
|
17
|
+
- Fix a bug in Result#next
|
18
|
+
- Fix Date type conversion
|
19
|
+
|
20
|
+
|
21
|
+
## v0.1.0 [2025-06-17] Michael Granger <ged@FaerieMUD.org>
|
22
|
+
|
23
|
+
Enhancements:
|
24
|
+
|
25
|
+
- Add Connection#database
|
26
|
+
- Squelch some warnings
|
27
|
+
- Add Result#tuples and Result#[].
|
28
|
+
|
29
|
+
|
5
30
|
## v0.0.3 [2025-05-22] Michael Granger <ged@FaerieMUD.org>
|
6
31
|
|
7
32
|
Fixes:
|
data/README.md
CHANGED
@@ -47,15 +47,129 @@ Once you have a Kuzu::Database object, you need a connection to actually use it:
|
|
47
47
|
# => #<Kuzu::Connection:0x0000000122a41f28 threads:16>
|
48
48
|
|
49
49
|
|
50
|
-
|
51
50
|
### Querying
|
52
51
|
|
52
|
+
There are two ways of running queries, immediate execution and via a prepared
|
53
|
+
statement. Either method returns one or more results as Kuzu::Result objects.
|
54
|
+
|
55
|
+
|
53
56
|
### Results
|
54
57
|
|
58
|
+
The each result of a query in a query string is returned as a Kuzu::Result. If
|
59
|
+
there are more than one queries in the query string, the Results will be
|
60
|
+
chained together so they can be iterated over.
|
61
|
+
|
62
|
+
Each tuple in a Result can be fetched using Kuzu::Result#next, which returns a
|
63
|
+
Hash of the variables in the `RETURN` clause, keyed by the column name as a
|
64
|
+
`String`:
|
65
|
+
|
66
|
+
res = conn.query( 'MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since;' )
|
67
|
+
# => #<Kuzu::Result:0x00000001268449d8 success: true (4 tuples of 3 columns)>
|
68
|
+
res.next
|
69
|
+
# => {"a.name" => "Adam", "b.name" => "Karissa", "f.since" => 2020}
|
70
|
+
res.next
|
71
|
+
# => {"a.name" => "Adam", "b.name" => "Zhang", "f.since" => 2020}
|
72
|
+
|
73
|
+
The value's type will be determined by the Kuzu datatype of that column of the
|
74
|
+
result. See Kuzu::Result for a mapping of the types.
|
75
|
+
|
76
|
+
If there are more tuples to fetch, Kuzu::Result#has_next? will return `true`:
|
77
|
+
|
78
|
+
res.has_next?
|
79
|
+
# => true
|
80
|
+
res.next
|
81
|
+
# => {"a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021}
|
82
|
+
res.next
|
83
|
+
# => {"a.name" => "Zhang", "b.name" => "Noura", "f.since" => 2022}
|
84
|
+
res.has_next?
|
85
|
+
# => false
|
86
|
+
res.next
|
87
|
+
# => nil
|
88
|
+
|
89
|
+
Kuzu::Result is also Enumerable, so you can `#each` over its tuples:
|
90
|
+
|
91
|
+
res.each
|
92
|
+
# => #<Enumerator: ...>
|
93
|
+
res.map do |tuple|
|
94
|
+
"%s has followed %s since %s" % tuple.values_at('a.name', 'b.name', 'f.since')
|
95
|
+
end
|
96
|
+
# => ["Adam has followed Karissa since 2020",
|
97
|
+
# "Adam has followed Zhang since 2020",
|
98
|
+
# "Karissa has followed Zhang since 2021",
|
99
|
+
# "Zhang has followed Noura since 2022"]
|
100
|
+
|
101
|
+
If the query string has more than one query in it, a separate Kuzu::Result is
|
102
|
+
linked to the previous one, and can be fetched using Kuzu::Result#next_set.
|
103
|
+
|
104
|
+
For example:
|
105
|
+
|
106
|
+
res = conn.query( <<~END_OF_QUERY )
|
107
|
+
MATCH (a:User)-[f:Follows]->(b:User)
|
108
|
+
RETURN a.name, b.name, f.since;
|
109
|
+
MATCH (u:User) RETURN *
|
110
|
+
END_OF_QUERY
|
111
|
+
# => #<Kuzu::Result:0x000000011e6faaf8 success: true (4 tuples of 3 columns)>
|
112
|
+
res.to_a
|
113
|
+
# => [{"a.name" => "Adam", "b.name" => "Karissa", "f.since" => 2020},
|
114
|
+
# {"a.name" => "Adam", "b.name" => "Zhang", "f.since" => 2020},
|
115
|
+
# {"a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021},
|
116
|
+
# {"a.name" => "Zhang", "b.name" => "Noura", "f.since" => 2022}]
|
117
|
+
res.has_next_set?
|
118
|
+
# => true
|
119
|
+
res2 = res.next_set
|
120
|
+
# => #<Kuzu::Result:0x000000011e338fa0 success: true (4 tuples of 1 columns)>
|
121
|
+
res2.to_a
|
122
|
+
# => [{"u" => #<Kuzu::Node:0x000000011e7553b8 @id=[0, 0], @label="User",
|
123
|
+
# @properties={name: "Adam", age: 30}>},
|
124
|
+
# {"u" => #<Kuzu::Node:0x000000011e7551b0 @id=[0, 1], @label="User",
|
125
|
+
# @properties={name: "Karissa", age: 40}>},
|
126
|
+
# {"u" => #<Kuzu::Node:0x000000011e755048 @id=[0, 2], @label="User",
|
127
|
+
# @properties={name: "Zhang", age: 50}>},
|
128
|
+
# {"u" => #<Kuzu::Node:0x000000011e754ee0 @id=[0, 3], @label="User",
|
129
|
+
# @properties={name: "Noura", age: 25}>}]
|
130
|
+
res2.has_next_set?
|
131
|
+
# => false
|
132
|
+
res2.next_set
|
133
|
+
# => nil
|
134
|
+
|
135
|
+
|
55
136
|
### Prepared Statements
|
56
137
|
|
57
|
-
|
138
|
+
An alternative to using strings to query the database is to used *prepared statements*. These have a number of advantages, such as reusability and using parameters for queries instead of string interpolation.
|
139
|
+
|
140
|
+
You can create a Kuzu::PreparedStatement by calling Kuzu::Connection#prepare, then execute it one or more times with parameters using Kuzu::PreparedStatement#execute:
|
141
|
+
|
142
|
+
stmt = conn.prepare( <<~END_OF_QUERY )
|
143
|
+
MATCH (a:User)-[f:Follows]->(b:User)
|
144
|
+
WHERE a.name = $name
|
145
|
+
RETURN a.name, b.name, f.since'
|
146
|
+
END_OF_QUERY
|
147
|
+
# => #<Kuzu::PreparedStatement:0x000000011e919b68>
|
148
|
+
res = stmt.execute( name: "Karissa" )
|
149
|
+
# => #<Kuzu::Result:0x000000011ee8df90 success: true (1 tuples of 3 columns)>
|
150
|
+
res.to_a
|
151
|
+
# => [{"a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021}]
|
152
|
+
|
58
153
|
|
154
|
+
### Result Memory Management
|
155
|
+
|
156
|
+
Because of the way Ruby frees memory when it's shutting down (i.e., the order is indeterminate), Kuzu::Result objects may not be freed immediately when they go out of scope. To provide some way to manage this, the Kuzu::Result#finish call is provided as a way to explicitly destroy the underlying Kuzu data structure so the Result can be freed. Since this is somewhat inconvenient to manage, there are two forms of Kuzu::Connection#query and Kuzu::PreparedStatement#execute, one which returns a Result and a "bang" equivalent one which just returns success or failure. Additionally, passing a block to either method will yield the Result to the block and then immediately `finish` the Result for you and return the block's value.
|
157
|
+
|
158
|
+
query_string = 'MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since'
|
159
|
+
conn.query!( query_string )
|
160
|
+
# => true
|
161
|
+
conn.query( query_string ) {|res| res.tuples }
|
162
|
+
# => [{"a.name" => "Adam", "b.name" => "Karissa", "f.since" => 2020},
|
163
|
+
# {"a.name" => "Adam", "b.name" => "Zhang", "f.since" => 2020},
|
164
|
+
# {"a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021},
|
165
|
+
# {"a.name" => "Zhang", "b.name" => "Noura", "f.since" => 2022}]
|
166
|
+
|
167
|
+
stmt = conn.prepare( "CREATE (:User {name: $name, age: $age})" )
|
168
|
+
# => #<Kuzu::PreparedStatement:0x000000010bc7cfe8>
|
169
|
+
stmt.execute!( name: 'David', age: 19 )
|
170
|
+
# => true
|
171
|
+
stmt.execute!( name: 'Agnes', age: 28 )
|
172
|
+
# => true
|
59
173
|
|
60
174
|
|
61
175
|
## Examples
|
@@ -64,26 +178,36 @@ Once you have a Kuzu::Database object, you need a connection to actually use it:
|
|
64
178
|
|
65
179
|
db = Kuzu.database
|
66
180
|
conn = db.connect
|
67
|
-
conn.
|
68
|
-
conn.
|
69
|
-
conn.
|
70
|
-
conn.
|
181
|
+
conn.run("CREATE NODE TABLE User(name STRING, age INT64, PRIMARY KEY (name))")
|
182
|
+
conn.run("CREATE NODE TABLE City(name STRING, population INT64, PRIMARY KEY (name))")
|
183
|
+
conn.run("CREATE REL TABLE Follows(FROM User TO User, since INT64)")
|
184
|
+
conn.run("CREATE REL TABLE LivesIn(FROM User TO City)")
|
71
185
|
|
72
186
|
# Load data.
|
73
|
-
conn.
|
74
|
-
conn.
|
75
|
-
conn.
|
76
|
-
conn.
|
187
|
+
conn.run("COPY User FROM \"user.csv\"")
|
188
|
+
conn.run("COPY City FROM \"city.csv\"")
|
189
|
+
conn.run("COPY Follows FROM \"follows.csv\"")
|
190
|
+
conn.run("COPY LivesIn FROM \"lives-in.csv\"")
|
77
191
|
|
78
192
|
# Execute a simple query.
|
79
193
|
result = conn.query("MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;")
|
80
194
|
|
81
195
|
# Output query result.
|
82
196
|
result.each do |tuple|
|
83
|
-
name, since, name2 = tuple.values_at(
|
197
|
+
name, since, name2 = tuple.values_at( 'a.name', 'f.since', 'b.name' )
|
84
198
|
puts "%s follows %s since %lld", [ name, name2, since ]
|
85
199
|
end
|
86
200
|
|
201
|
+
result.finish
|
202
|
+
|
203
|
+
|
204
|
+
## To-Do List
|
205
|
+
|
206
|
+
- `UNION` result type.
|
207
|
+
- `JSON` result type from the JSON extension
|
208
|
+
- Better memory management for Kuzu::Results
|
209
|
+
|
210
|
+
|
87
211
|
## Requirements
|
88
212
|
|
89
213
|
- Ruby >= 3
|
data/ext/kuzu_ext/connection.c
CHANGED
@@ -170,7 +170,10 @@ rkuzu_connection_do_query( VALUE self, VALUE query )
|
|
170
170
|
rkuzu_connection_do_query_without_gvl, (void *)&qcall,
|
171
171
|
rkuzu_connection_cancel_query, (void *)&conn->conn );
|
172
172
|
|
173
|
+
_Pragma("GCC diagnostic push")
|
174
|
+
_Pragma("GCC diagnostic ignored \"-Wvoid-pointer-to-enum-cast\"")
|
173
175
|
query_state = (kuzu_state)result_ptr;
|
176
|
+
_Pragma("GCC diagnostic pop")
|
174
177
|
|
175
178
|
if ( query_state != KuzuSuccess ) {
|
176
179
|
char *err_detail = kuzu_query_result_get_error_message( &result );
|
@@ -278,6 +281,21 @@ rkuzu_connection_query_timeout_eq( VALUE self, VALUE timeout )
|
|
278
281
|
}
|
279
282
|
|
280
283
|
|
284
|
+
/*
|
285
|
+
* call-seq:
|
286
|
+
* connection.database -> database
|
287
|
+
*
|
288
|
+
* Return the database object the connection belongs to (a Kuzu::Database).
|
289
|
+
*
|
290
|
+
*/
|
291
|
+
static VALUE
|
292
|
+
rkuzu_connection_database( VALUE self )
|
293
|
+
{
|
294
|
+
rkuzu_connection *ptr = CHECK_CONNECTION( self );
|
295
|
+
return ptr->database;
|
296
|
+
}
|
297
|
+
|
298
|
+
|
281
299
|
|
282
300
|
/*
|
283
301
|
* Document-class: Kuzu::Connection
|
@@ -306,5 +324,8 @@ rkuzu_init_connection( void )
|
|
306
324
|
|
307
325
|
rb_define_method( rkuzu_cKuzuConnection, "query_timeout=", rkuzu_connection_query_timeout_eq, 1 );
|
308
326
|
|
327
|
+
rb_define_method( rkuzu_cKuzuConnection, "database", rkuzu_connection_database, 0 );
|
328
|
+
rb_define_alias( rkuzu_cKuzuConnection, "db", "database" );
|
329
|
+
|
309
330
|
rb_require( "kuzu/connection" );
|
310
331
|
}
|
data/ext/kuzu_ext/kuzu_ext.h
CHANGED
@@ -28,8 +28,10 @@
|
|
28
28
|
#ifdef HAVE_STDARG_PROTOTYPES
|
29
29
|
#include <stdarg.h>
|
30
30
|
#define va_init_list(a, b) va_start (a, b)
|
31
|
-
void rkuzu_log_obj (VALUE, const char *, const char *, ...)
|
32
|
-
|
31
|
+
void rkuzu_log_obj (VALUE, const char *, const char *, ...)
|
32
|
+
__attribute__ ((format (printf, 2, 0)));
|
33
|
+
void rkuzu_log (const char *, const char *, ...)
|
34
|
+
__attribute__ ((format (printf, 1, 0)));
|
33
35
|
#else
|
34
36
|
#include <varargs.h>
|
35
37
|
#define va_init_list(a, b) va_start (a)
|
@@ -43,7 +43,7 @@ rkuzu_get_prepared_statement( VALUE prepared_statement_obj )
|
|
43
43
|
* Allocation function
|
44
44
|
*/
|
45
45
|
static rkuzu_prepared_statement *
|
46
|
-
rkuzu_prepared_statement_alloc()
|
46
|
+
rkuzu_prepared_statement_alloc( void )
|
47
47
|
{
|
48
48
|
rkuzu_prepared_statement *ptr = ALLOC( rkuzu_prepared_statement );
|
49
49
|
|
@@ -181,7 +181,11 @@ rkuzu_prepared_statement_do_execute( VALUE self )
|
|
181
181
|
result_ptr = rb_thread_call_without_gvl(
|
182
182
|
rkuzu_connection_do_execute_without_gvl, (void *)&qcall,
|
183
183
|
rkuzu_connection_cancel_execute, (void *)&conn->conn );
|
184
|
+
|
185
|
+
_Pragma("GCC diagnostic push")
|
186
|
+
_Pragma("GCC diagnostic ignored \"-Wvoid-pointer-to-enum-cast\"")
|
184
187
|
execute_state = (kuzu_state)result_ptr;
|
188
|
+
_Pragma("GCC diagnostic pop")
|
185
189
|
|
186
190
|
if ( execute_state != KuzuSuccess ) {
|
187
191
|
char *err_detail = kuzu_query_result_get_error_message( &result );
|
@@ -51,7 +51,7 @@ rkuzu_query_summary_s_allocate( VALUE klass )
|
|
51
51
|
* call-seq:
|
52
52
|
* Kuzu::QuerySummary.from_result( result ) -> query_summary
|
53
53
|
*
|
54
|
-
* Return a Kuzu::QuerySummary from a Kuzu::
|
54
|
+
* Return a Kuzu::QuerySummary from a Kuzu::Result.
|
55
55
|
*
|
56
56
|
*/
|
57
57
|
static VALUE
|
data/ext/kuzu_ext/result.c
CHANGED
@@ -408,7 +408,7 @@ rkuzu_result_get_column_names( VALUE self )
|
|
408
408
|
|
409
409
|
for ( uint64_t i = 0 ; i < col_count ; i++ ) {
|
410
410
|
if ( kuzu_query_result_get_column_name(&result->result, i, &name) != KuzuSuccess ) {
|
411
|
-
rb_raise( rkuzu_eError, "couldn't fetch name of column %
|
411
|
+
rb_raise( rkuzu_eError, "couldn't fetch name of column %lu", i );
|
412
412
|
}
|
413
413
|
rb_ary_push( rval, rb_str_new2(name) );
|
414
414
|
}
|
data/ext/kuzu_ext/types.c
CHANGED
@@ -165,8 +165,8 @@ rkuzu_convert_date( kuzu_value *value )
|
|
165
165
|
|
166
166
|
kuzu_date_to_tm( typed_value, &time );
|
167
167
|
|
168
|
-
argv[0] = INT2FIX( time.tm_year );
|
169
|
-
argv[1] = INT2FIX( time.tm_mon );
|
168
|
+
argv[0] = INT2FIX( time.tm_year + 1900 );
|
169
|
+
argv[1] = INT2FIX( time.tm_mon + 1 );
|
170
170
|
argv[2] = INT2FIX( time.tm_mday );
|
171
171
|
|
172
172
|
return rb_class_new_instance( 3, argv, rkuzu_rb_cDate );
|
@@ -554,6 +554,9 @@ rkuzu_convert_kuzu_value_to_ruby( kuzu_data_type_id type_id, kuzu_value *value )
|
|
554
554
|
case KUZU_UINT8: return rkuzu_convert_uint8( value );
|
555
555
|
case KUZU_INT128: return rkuzu_convert_int128( value );
|
556
556
|
|
557
|
+
// Serials just come out as int64s
|
558
|
+
case KUZU_SERIAL: return rkuzu_convert_int64( value );
|
559
|
+
|
557
560
|
case KUZU_DOUBLE: return rkuzu_convert_double( value );
|
558
561
|
case KUZU_FLOAT: return rkuzu_convert_float( value );
|
559
562
|
|
@@ -18,6 +18,8 @@ class Kuzu::PreparedStatement
|
|
18
18
|
### then finished automatically, and the return value of the block returned
|
19
19
|
### instead.
|
20
20
|
def execute( **bound_variables, &block )
|
21
|
+
self.log.debug "Executing statement:\n%s\nwith variables:\n%p" %
|
22
|
+
[ self.query, bound_variables ]
|
21
23
|
self.bind( **bound_variables )
|
22
24
|
result = self._execute
|
23
25
|
return Kuzu::Result.wrap_block_result( result, &block )
|
data/lib/kuzu/result.rb
CHANGED
@@ -6,6 +6,47 @@ require 'kuzu' unless defined?( Kuzu )
|
|
6
6
|
|
7
7
|
|
8
8
|
# Kùzu query result class
|
9
|
+
#
|
10
|
+
# These objects contain one result set from either a Kuzu::Connection#query call
|
11
|
+
# or Kuzu::PreparedStatement#execute. If there are multiple result sets, you can
|
12
|
+
# fetch the next one by calling Kuzu::Result#next_set. You can use #has_next_set?
|
13
|
+
# to test for a following set.
|
14
|
+
#
|
15
|
+
# Tuple values are converted to corresponding Ruby objects:
|
16
|
+
#
|
17
|
+
# | Kuzu Type | Ruby Type |
|
18
|
+
# | --------------- | ----------------------------------------------- |
|
19
|
+
# | +INT8+ | +Integer+ |
|
20
|
+
# | +INT16+ | +Integer+ |
|
21
|
+
# | +INT32+ | +Integer+ |
|
22
|
+
# | +INT64+ | +Integer+ |
|
23
|
+
# | +INT128+ | +Integer+ |
|
24
|
+
# | +UINT8+ | +Integer+ |
|
25
|
+
# | +UINT16+ | +Integer+ |
|
26
|
+
# | +UINT32+ | +Integer+ |
|
27
|
+
# | +UINT64+ | +Integer+ |
|
28
|
+
# | +FLOAT+ | +Float+ |
|
29
|
+
# | +DOUBLE+ | +Float+ |
|
30
|
+
# | +DECIMAL+ | +Float+ |
|
31
|
+
# | +BOOLEAN+ | +TrueClass+ or +FalseClass+ |
|
32
|
+
# | +UUID+ | +String+ (UTF-8 encoding) |
|
33
|
+
# | +STRING+ | +String+ (UTF-8 encoding) |
|
34
|
+
# | +NULL+ | +NilClass+ |
|
35
|
+
# | +DATE+ | +Date+ |
|
36
|
+
# | +TIMESTAMP+ | +Time+ |
|
37
|
+
# | +INTERVAL+ | +Float+ (interval in seconds) |
|
38
|
+
# | +STRUCT+ | +OpenStruct+ via the +ostruct+ standard library |
|
39
|
+
# | +MAP+ | +Hash+ |
|
40
|
+
# | +UNION+ | (not yet handled) |
|
41
|
+
# | +BLOB+ | +String+ (+ASCII_8BIT+ encoding) |
|
42
|
+
# | +SERIAL+ | +Integer+ |
|
43
|
+
# | +NODE+ | Kuzu::Node |
|
44
|
+
# | +REL+ | Kuzu::Rel |
|
45
|
+
# | +RECURSIVE_REL+ | Kuzu::RecursiveRel |
|
46
|
+
# | +LIST+ | +Array+ |
|
47
|
+
# | +ARRAY+ | +Array+ |
|
48
|
+
#
|
49
|
+
#
|
9
50
|
class Kuzu::Result
|
10
51
|
extend Loggability
|
11
52
|
|
@@ -60,7 +101,8 @@ class Kuzu::Result
|
|
60
101
|
|
61
102
|
### Get the next tuple of the result as a Hash.
|
62
103
|
def next
|
63
|
-
|
104
|
+
values = self.get_next_values or return nil
|
105
|
+
pairs = self.column_names.zip( values )
|
64
106
|
return Hash[ pairs ]
|
65
107
|
end
|
66
108
|
|
@@ -74,6 +116,19 @@ class Kuzu::Result
|
|
74
116
|
end
|
75
117
|
|
76
118
|
|
119
|
+
### Return the tuples from the current result set. This method is memoized
|
120
|
+
### for efficiency.
|
121
|
+
def tuples
|
122
|
+
return @_tuples ||= self.to_a
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Index operator: fetch the tuple at +index+ of the current result set.
|
127
|
+
def []( index )
|
128
|
+
return self.tuples[ index ]
|
129
|
+
end
|
130
|
+
|
131
|
+
|
77
132
|
### Return the next result set after this one as a Kuzu::Result, or `nil`if
|
78
133
|
### there is no next set.
|
79
134
|
def next_set
|
@@ -97,11 +152,10 @@ class Kuzu::Result
|
|
97
152
|
if self.finished?
|
98
153
|
details = " (finished)"
|
99
154
|
else
|
100
|
-
details = " success: %p (%d tuples of %d columns)
|
155
|
+
details = " success: %p (%d tuples of %d columns)" % [
|
101
156
|
self.success?,
|
102
157
|
self.num_tuples,
|
103
158
|
self.num_columns,
|
104
|
-
self.to_s,
|
105
159
|
]
|
106
160
|
end
|
107
161
|
|
@@ -116,6 +170,7 @@ class Kuzu::Result
|
|
116
170
|
|
117
171
|
### Return an Enumerator that yields result tuples as Hashes.
|
118
172
|
def tuple_enum
|
173
|
+
self.log.debug "Fetching a tuple Enumerator"
|
119
174
|
return Enumerator.new do |yielder|
|
120
175
|
self.reset_iterator
|
121
176
|
while self.has_next?
|
@@ -126,7 +181,9 @@ class Kuzu::Result
|
|
126
181
|
end
|
127
182
|
|
128
183
|
|
184
|
+
### Return an Enumerator that yields a Result for each set.
|
129
185
|
def next_set_enum
|
186
|
+
self.log.debug "Fetching a result set Enumerator"
|
130
187
|
result = self
|
131
188
|
return Enumerator.new do |yielder|
|
132
189
|
while result
|
data/lib/kuzu.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# -*- ruby -*-
|
2
2
|
|
3
|
+
require 'pathname'
|
3
4
|
require 'loggability'
|
4
5
|
|
5
6
|
require_relative 'kuzu_ext'
|
@@ -11,7 +12,10 @@ module Kuzu
|
|
11
12
|
|
12
13
|
|
13
14
|
# Library version
|
14
|
-
VERSION = '0.0
|
15
|
+
VERSION = '0.2.0'
|
16
|
+
|
17
|
+
# Name of the file to look for when testing a path to see if it's a Kuzu database.
|
18
|
+
KUZU_CATALOG_FILENAME = 'data.kz'
|
15
19
|
|
16
20
|
|
17
21
|
# Set up a logger for Kuzu classes
|
@@ -52,6 +56,23 @@ module Kuzu
|
|
52
56
|
end
|
53
57
|
|
54
58
|
|
59
|
+
### Returns +true+ if the specified +pathname+ appears to be a valid Kuzu database
|
60
|
+
### for the current version of the storage format.
|
61
|
+
def self::is_database?( pathname )
|
62
|
+
pathname = Pathname( pathname )
|
63
|
+
if Kuzu.storage_version <= 38
|
64
|
+
return false unless pathname.directory?
|
65
|
+
testfile = pathname / KUZU_CATALOG_FILENAME
|
66
|
+
return testfile.exist?
|
67
|
+
else
|
68
|
+
return false unless pathname.file?
|
69
|
+
magic = pathname.read( 5 )
|
70
|
+
return magic[0, 4] == 'KUZU' && magic[4, 1].ord == Kuzu.storage_version
|
71
|
+
end
|
72
|
+
end
|
73
|
+
singleton_class.alias_method( :is_kuzu_database?, :is_database? )
|
74
|
+
|
75
|
+
|
55
76
|
### Return a Time object from the given +milliseconds+ epoch time.
|
56
77
|
def self::timestamp_from_timestamp_ms( milliseconds )
|
57
78
|
seconds, subsec = milliseconds.divmod( 1_000 )
|
@@ -74,4 +95,3 @@ module Kuzu
|
|
74
95
|
end
|
75
96
|
|
76
97
|
end # module Kuzu
|
77
|
-
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'kuzu/connection'
|
6
|
+
|
7
|
+
|
8
|
+
RSpec.describe( Kuzu::Connection ) do
|
9
|
+
|
10
|
+
let( :db ) { Kuzu.database }
|
11
|
+
|
12
|
+
|
13
|
+
it "can set the maximum number of threads for execution" do
|
14
|
+
connection = db.connect
|
15
|
+
|
16
|
+
expect {
|
17
|
+
connection.max_num_threads_for_exec += 1
|
18
|
+
}.to change { connection.max_num_threads_for_exec }.by( 1 )
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
it "shows the number of threads used for execution when inspected" do
|
23
|
+
connection = db.connect
|
24
|
+
|
25
|
+
expect( connection.inspect ).to match( /threads:\d+/i )
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
it "knows what database it's a connection for" do
|
30
|
+
connection = db.connect
|
31
|
+
|
32
|
+
expect( connection.database ).to eq( db )
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
data/spec/kuzu/database_spec.rb
CHANGED
@@ -8,12 +8,18 @@ require 'kuzu/database'
|
|
8
8
|
RSpec.describe( Kuzu::Database ) do
|
9
9
|
|
10
10
|
let( :spec_tmpdir ) do
|
11
|
-
tmpfile_pathname()
|
11
|
+
path = tmpfile_pathname()
|
12
|
+
path.mkpath
|
13
|
+
return path
|
12
14
|
end
|
13
15
|
|
14
16
|
let( :db_path ) { spec_tmpdir + 'spec_db' }
|
15
17
|
|
16
18
|
|
19
|
+
after( :each ) do
|
20
|
+
GC.start
|
21
|
+
end
|
22
|
+
|
17
23
|
it "can be created in-memory" do
|
18
24
|
instance = described_class.new( '' )
|
19
25
|
expect( instance ).to be_a( described_class )
|
@@ -21,7 +27,7 @@ RSpec.describe( Kuzu::Database ) do
|
|
21
27
|
|
22
28
|
|
23
29
|
it "can be created read-only from an existing on-disk database" do
|
24
|
-
|
30
|
+
_original = described_class.new( db_path.to_s )
|
25
31
|
|
26
32
|
ro = described_class.new( db_path.to_s, read_only: true )
|
27
33
|
expect( ro ).to be_read_only
|
data/spec/kuzu/result_spec.rb
CHANGED
@@ -78,7 +78,7 @@ RSpec.describe( Kuzu::Result ) do
|
|
78
78
|
expect( result ).to be_success
|
79
79
|
expect( result.num_columns ).to eq( 3 )
|
80
80
|
expect( result.column_names ).to eq([ "a.name", "b.name", "f.since" ])
|
81
|
-
expect( result.
|
81
|
+
expect( result.to_a ).to eq([
|
82
82
|
{ "a.name" => "Adam", "b.name" => "Karissa", "f.since" => 2020 },
|
83
83
|
{ "a.name" => "Adam", "b.name" => "Zhang", "f.since" => 2020 },
|
84
84
|
{ "a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021 },
|
@@ -89,6 +89,41 @@ RSpec.describe( Kuzu::Result ) do
|
|
89
89
|
end
|
90
90
|
|
91
91
|
|
92
|
+
it "handles a #next after it finishes iteration over the current set" do
|
93
|
+
setup_demo_db()
|
94
|
+
|
95
|
+
result = described_class.from_query( connection, <<~END_OF_QUERY )
|
96
|
+
MATCH ( a:User )-[ f:Follows ]->( b:User )
|
97
|
+
RETURN a.name, b.name, f.since;
|
98
|
+
END_OF_QUERY
|
99
|
+
|
100
|
+
result.tuples # Iterate over all tuples
|
101
|
+
|
102
|
+
expect( result.next ).to be_nil
|
103
|
+
|
104
|
+
result.finish
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
it "can fetch individual result tuples via the index operator" do
|
109
|
+
setup_demo_db()
|
110
|
+
|
111
|
+
result = described_class.from_query( connection, <<~END_OF_QUERY )
|
112
|
+
MATCH ( a:User )-[ f:Follows ]->( b:User )
|
113
|
+
RETURN a.name, b.name, f.since;
|
114
|
+
END_OF_QUERY
|
115
|
+
|
116
|
+
tuples = result.to_a
|
117
|
+
|
118
|
+
expect( result[0] ).to eq( tuples[0] )
|
119
|
+
expect( result[1] ).to eq( tuples[1] )
|
120
|
+
expect( result[2] ).to eq( tuples[2] )
|
121
|
+
expect( result[3] ).to eq( tuples[3] )
|
122
|
+
|
123
|
+
result.finish
|
124
|
+
end
|
125
|
+
|
126
|
+
|
92
127
|
it "can iterate over result sets" do
|
93
128
|
result = described_class.from_query( connection, <<~END_QUERY )
|
94
129
|
return 1;
|
@@ -97,7 +132,7 @@ RSpec.describe( Kuzu::Result ) do
|
|
97
132
|
END_QUERY
|
98
133
|
|
99
134
|
rval = result.each_set.flat_map do |subset|
|
100
|
-
subset.
|
135
|
+
subset.to_a
|
101
136
|
end
|
102
137
|
|
103
138
|
expect( rval ).to eq([
|
data/spec/kuzu/types_spec.rb
CHANGED
@@ -34,6 +34,25 @@ RSpec.describe( "data types" ) do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
|
37
|
+
it "coverts DATE values to Date objects" do
|
38
|
+
result = connection.query( %{RETURN CAST('2025-07-14', 'DATE') as x;} )
|
39
|
+
|
40
|
+
expect( result ).to be_a( Kuzu::Result )
|
41
|
+
expect( result ).to be_success
|
42
|
+
|
43
|
+
value = result.first
|
44
|
+
expect( value ).to include( 'x' )
|
45
|
+
|
46
|
+
x = value['x']
|
47
|
+
expect( x ).to be_a( Date )
|
48
|
+
expect( x.year ).to eq( 2025 )
|
49
|
+
expect( x.month ).to eq( 7 )
|
50
|
+
expect( x.day ).to eq( 14 )
|
51
|
+
|
52
|
+
result.finish
|
53
|
+
end
|
54
|
+
|
55
|
+
|
37
56
|
it "converts STRUCT values to OpenStructs" do
|
38
57
|
result = connection.query( "RETURN {first: 'Adam', last: 'Smith'} AS record;" )
|
39
58
|
|
@@ -251,4 +270,15 @@ RSpec.describe( "data types" ) do
|
|
251
270
|
end
|
252
271
|
|
253
272
|
|
273
|
+
it "converts SERIAL types to Integer objects" do
|
274
|
+
result = connection.query( %{RETURN CAST(133, "SERIAL") AS s;} )
|
275
|
+
rval = result.first['s']
|
276
|
+
|
277
|
+
expect( rval ).to be_an( Integer )
|
278
|
+
expect( rval ).to eq( 133 )
|
279
|
+
|
280
|
+
result.finish
|
281
|
+
end
|
282
|
+
|
283
|
+
|
254
284
|
end
|
data/spec/kuzu_spec.rb
CHANGED
@@ -7,7 +7,9 @@ require 'kuzu'
|
|
7
7
|
RSpec.describe( Kuzu ) do
|
8
8
|
|
9
9
|
let( :spec_tmpdir ) do
|
10
|
-
tmpfile_pathname()
|
10
|
+
path = tmpfile_pathname()
|
11
|
+
path.mkpath
|
12
|
+
return path
|
11
13
|
end
|
12
14
|
|
13
15
|
|
@@ -49,7 +51,33 @@ RSpec.describe( Kuzu ) do
|
|
49
51
|
result = described_class.database( filename )
|
50
52
|
|
51
53
|
expect( result ).to be_a( Kuzu::Database )
|
52
|
-
|
54
|
+
if Kuzu.storage_version <= 38
|
55
|
+
expect( filename ).to be_a_directory
|
56
|
+
else
|
57
|
+
expect( filename ).to be_a_file
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
it "can tell whether a string looks like a path to a Kuzu database" do
|
63
|
+
path = spec_tmpdir + 'spec_db'
|
64
|
+
|
65
|
+
expect {
|
66
|
+
described_class.database( path )
|
67
|
+
}.to change {
|
68
|
+
described_class.is_database?( path.to_s )
|
69
|
+
}.from( false ).to( true )
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
it "can tell whether a Pathname looks like a path to a Kuzu database" do
|
74
|
+
path = spec_tmpdir + 'spec_db'
|
75
|
+
|
76
|
+
expect {
|
77
|
+
described_class.database( path )
|
78
|
+
}.to change {
|
79
|
+
described_class.is_database?( path )
|
80
|
+
}.from( false ).to( true )
|
53
81
|
end
|
54
82
|
|
55
83
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -54,7 +54,7 @@ module Kuzu::SpecHelpers
|
|
54
54
|
|
55
55
|
### Return a Pathname pointing to a temporary file.
|
56
56
|
def tmpfile_pathname( filetype='spec' )
|
57
|
-
Pathname(Dir::Tmpname.create(['kuzu-', '-test-' + filetype]) {})
|
57
|
+
return Pathname( Dir::Tmpname.create(['kuzu-', '-test-' + filetype]) {} )
|
58
58
|
end
|
59
59
|
|
60
60
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-kuzu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Granger
|
@@ -34,7 +34,7 @@ cert_chain:
|
|
34
34
|
8qAqdfV+4u6Huu1KzAuDQCheyEyISsLST37sU/irV3czV6BiFipWag1XiJciRT3A
|
35
35
|
wZqCfTNVHTdtsCbfdA1DsA3RdG2iEH3TOHzv1Rqzqh4=
|
36
36
|
-----END CERTIFICATE-----
|
37
|
-
date: 2025-
|
37
|
+
date: 2025-07-16 00:00:00.000000000 Z
|
38
38
|
dependencies:
|
39
39
|
- !ruby/object:Gem::Dependency
|
40
40
|
name: rake-compiler
|
@@ -127,6 +127,7 @@ files:
|
|
127
127
|
- lib/kuzu/rel.rb
|
128
128
|
- lib/kuzu/result.rb
|
129
129
|
- spec/kuzu/config_spec.rb
|
130
|
+
- spec/kuzu/connection_spec.rb
|
130
131
|
- spec/kuzu/database_spec.rb
|
131
132
|
- spec/kuzu/prepared_statement_spec.rb
|
132
133
|
- spec/kuzu/query_summary_spec.rb
|
@@ -157,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
158
|
- !ruby/object:Gem::Version
|
158
159
|
version: '0'
|
159
160
|
requirements: []
|
160
|
-
rubygems_version: 3.6.
|
161
|
+
rubygems_version: 3.6.9
|
161
162
|
specification_version: 4
|
162
163
|
summary: A Ruby binding for the Kùzu embedded graph database.
|
163
164
|
test_files: []
|
metadata.gz.sig
CHANGED
@@ -1,2 +1,3 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
J�DZ#\A3��K���&��E����Q���dEcߋM� 9��8S�����Y��"_���'����N����A'8S�V 8��(�u��y�}bO��ҹG�ņ���C˽h؇�4��S� \�.�)9
|
2
|
+
���{��~86�#+�tu����R�����_�}x��<gx8�;)a����U��%��
|
3
|
+
|��r�7˫�<}nVt�ml�SN�%Ѳ��4������j��F���n^M���������ZG��0N��D�����N���8�ϔU��u�����P�g�q��qf�h�C��c�?2�A�owe�#�.
|