ruby-kuzu 0.0.1

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.
@@ -0,0 +1,93 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'kuzu/prepared_statement'
6
+
7
+
8
+ RSpec.describe( Kuzu::PreparedStatement ) do
9
+
10
+ let( :db ) { Kuzu.database }
11
+ let( :connection ) { db.connect }
12
+
13
+ let( :schema ) { load_test_data( 'demo-db/schema.cypher' ) }
14
+ let( :copy_statements ) { load_test_data( 'demo-db/copy.cypher' ) }
15
+
16
+
17
+ def setup_demo_db
18
+ connection.run( schema )
19
+ connection.run( copy_statements )
20
+ end
21
+
22
+
23
+ before( :each ) do
24
+ setup_demo_db()
25
+ end
26
+
27
+
28
+ #
29
+ # Specs
30
+ #
31
+
32
+ it "can be created via a connection and a query string" do
33
+ statement = described_class.new( connection, <<~END_OF_QUERY )
34
+ MATCH (u:User)
35
+ WHERE u.age > $min_age and u.age < $max_age
36
+ RETURN u.name
37
+ END_OF_QUERY
38
+
39
+ expect( statement ).to be_a( described_class )
40
+ expect( statement ).to be_success
41
+ expect( statement.connection ).to be( connection )
42
+ end
43
+
44
+
45
+ it "doesn't error if executed before its variables are bound" do
46
+ statement = described_class.new( connection, <<~END_OF_QUERY )
47
+ MATCH (u:User)
48
+ WHERE u.age > $min_age and u.age < $max_age
49
+ RETURN u.name
50
+ END_OF_QUERY
51
+
52
+ result = statement.execute
53
+
54
+ expect( result.num_tuples ).to eq( 0 )
55
+
56
+ result.finish
57
+ end
58
+
59
+
60
+ it "can be executed if all of its variables are bound" do
61
+ statement = described_class.new( connection, <<~END_OF_QUERY )
62
+ MATCH (u:User)
63
+ WHERE u.age >= $min_age and u.age <= $max_age
64
+ RETURN u.name
65
+ END_OF_QUERY
66
+
67
+ result = statement.execute( min_age: 40, max_age: 50 )
68
+
69
+ expect( result.num_tuples ).to eq( 2 )
70
+
71
+ result.finish
72
+ end
73
+
74
+
75
+ it "can be reused" do
76
+ statement = described_class.new( connection, <<~END_OF_QUERY )
77
+ MATCH (u:User)
78
+ WHERE u.age >= $min_age and u.age <= $max_age
79
+ RETURN u.name
80
+ END_OF_QUERY
81
+
82
+ result = statement.execute( min_age: 40, max_age: 50 )
83
+ expect( result ).to be_success
84
+ expect( result.num_tuples ).to eq( 2 )
85
+ result.finish
86
+
87
+ result = statement.execute( min_age: 5, max_age: 90 )
88
+ expect( result ).to be_success
89
+ expect( result.num_tuples ).to eq( 4 )
90
+ result.finish
91
+ end
92
+
93
+ end
@@ -0,0 +1,30 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'kuzu/query_summary'
6
+
7
+
8
+ RSpec.describe( Kuzu::QuerySummary ) do
9
+
10
+ CREATE_TABLE_STATEMENTS = [
11
+ 'CREATE NODE TABLE User(name STRING, age INT64, PRIMARY KEY (name))',
12
+ 'CREATE NODE TABLE City(name STRING, population INT64, PRIMARY KEY (name))',
13
+ ]
14
+
15
+
16
+ let( :db ) { Kuzu.database }
17
+ let( :connection ) { db.connect }
18
+
19
+
20
+ it "can return a summary of its query's timing" do
21
+ connection.query( CREATE_TABLE_STATEMENTS.first ) do |result|
22
+ summary = result.query_summary
23
+
24
+ expect( summary ).to be_a( described_class )
25
+ expect( summary.compiling_time ).to be_a( Float ).and( be > 0.0 )
26
+ expect( summary.execution_time ).to be_a( Float ).and( be > 0.0 )
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,190 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'kuzu/result'
6
+
7
+
8
+ RSpec.describe( Kuzu::Result ) do
9
+
10
+ let( :db ) { Kuzu.database }
11
+ let( :connection ) { db.connect }
12
+
13
+ let( :schema ) { load_test_data( 'demo-db/schema.cypher' ) }
14
+ let( :copy_statements ) { load_test_data( 'demo-db/copy.cypher' ) }
15
+
16
+
17
+ def setup_demo_db
18
+ connection.run( schema )
19
+ connection.run( copy_statements )
20
+ end
21
+
22
+
23
+ #
24
+ # Specs
25
+ #
26
+
27
+ describe "query constructor" do
28
+
29
+ it "can be created via a simple query" do
30
+ result = described_class.from_query( connection, schema.each_line.first )
31
+
32
+ expect( result ).to be_a( described_class )
33
+ expect( result ).to be_success
34
+
35
+ result.finish
36
+ end
37
+
38
+
39
+ it "automatically finishes the result when run with a block" do
40
+ block_result = nil
41
+
42
+ described_class.from_query( connection, schema.each_line.first ) do |result|
43
+ block_result = result
44
+ expect( result ).to be_a( described_class )
45
+ expect( result ).to be_success
46
+ end
47
+
48
+ expect( block_result ).to be_finished
49
+ end
50
+
51
+
52
+ it "can return a summary of its query's timing" do
53
+ result = described_class.from_query( connection, schema.each_line.first )
54
+ summary = result.query_summary
55
+
56
+ expect( summary ).to be_a( Kuzu::QuerySummary )
57
+
58
+ result.finish
59
+ end
60
+
61
+
62
+ it "can return the error message if there was a problem with the query" do
63
+ expect {
64
+ described_class.from_query( connection, "FOOBANGLE NERFRIDER" )
65
+ }.to raise_error( Kuzu::QueryError, /parser exception/i )
66
+ end
67
+
68
+
69
+ it "can iterate over result tuples" do
70
+ setup_demo_db()
71
+
72
+ result = described_class.from_query( connection, <<~END_OF_QUERY )
73
+ MATCH ( a:User )-[ f:Follows ]->( b:User )
74
+ RETURN a.name, b.name, f.since;
75
+ END_OF_QUERY
76
+
77
+ expect( result ).to be_a( described_class )
78
+ expect( result ).to be_success
79
+ expect( result.num_columns ).to eq( 3 )
80
+ expect( result.column_names ).to eq([ "a.name", "b.name", "f.since" ])
81
+ expect( result.each.to_a ).to eq([
82
+ { "a.name" => "Adam", "b.name" => "Karissa", "f.since" => 2020 },
83
+ { "a.name" => "Adam", "b.name" => "Zhang", "f.since" => 2020 },
84
+ { "a.name" => "Karissa", "b.name" => "Zhang", "f.since" => 2021 },
85
+ { "a.name" => "Zhang", "b.name" => "Noura", "f.since" => 2022 }
86
+ ])
87
+
88
+ result.finish
89
+ end
90
+
91
+
92
+ it "can iterate over result sets" do
93
+ result = described_class.from_query( connection, <<~END_QUERY )
94
+ return 1;
95
+ return 2;
96
+ return 3;
97
+ END_QUERY
98
+
99
+ rval = result.each_set.flat_map do |subset|
100
+ subset.each.to_a
101
+ end
102
+
103
+ expect( rval ).to eq([
104
+ { "1" => 1 },
105
+ { "2" => 2 },
106
+ { "3" => 3 },
107
+ ])
108
+
109
+ result.finish
110
+ end
111
+
112
+
113
+ it "also finishes successive results if they've been fetched when the first one is finished" do
114
+ result = described_class.from_query( connection, <<~END_QUERY )
115
+ return 1;
116
+ return 2;
117
+ return 3;
118
+ END_QUERY
119
+
120
+ second_result = result.next_set
121
+
122
+ result.finish
123
+
124
+ expect( second_result ).to be_finished
125
+ end
126
+
127
+
128
+ it "errors if it's used after being finished" do
129
+ result = described_class.from_query( connection, "return 1;" )
130
+ result.finish
131
+
132
+ expect( result ).to be_finished
133
+
134
+ expect {
135
+ result.get_next_values
136
+ }.to raise_error( Kuzu::FinishedError )
137
+ end
138
+
139
+ end
140
+
141
+
142
+ describe "prepared statement constructor" do
143
+
144
+ before( :each ) do
145
+ setup_demo_db()
146
+ end
147
+
148
+
149
+ it "can be created via a prepared statement" do
150
+ statement = Kuzu::PreparedStatement.new( connection, <<~END_OF_QUERY )
151
+ MATCH (u:User)
152
+ WHERE u.age > $min_age and u.age < $max_age
153
+ RETURN u.name
154
+ END_OF_QUERY
155
+ statement.bind( min_age: 32, max_age: 46 )
156
+
157
+ result = described_class.from_prepared_statement( statement )
158
+
159
+ expect( result ).to be_a( described_class )
160
+ expect( result ).to be_success
161
+
162
+ result.finish
163
+ end
164
+
165
+
166
+ it "is automatically finished if constructed with a block" do
167
+ statement = Kuzu::PreparedStatement.new( connection, <<~END_OF_QUERY )
168
+ MATCH (u:User)
169
+ WHERE u.age > $min_age and u.age < $max_age
170
+ RETURN u.name
171
+ END_OF_QUERY
172
+ statement.bind( min_age: 32, max_age: 46 )
173
+
174
+ block_result = nil
175
+ rval = described_class.from_prepared_statement( statement ) do |result|
176
+ block_result = result
177
+ expect( result ).to be_a( described_class )
178
+ expect( result ).to be_success
179
+
180
+ result.first['u.name']
181
+ end
182
+
183
+ expect( block_result ).to be_finished
184
+ expect( rval ).to eq( "Karissa" )
185
+ end
186
+
187
+ end
188
+
189
+
190
+ end
@@ -0,0 +1,254 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'kuzu'
6
+
7
+
8
+ RSpec.describe( "data types" ) do
9
+
10
+ let( :db ) { Kuzu.database }
11
+ let( :connection ) { db.connect }
12
+
13
+
14
+ #
15
+ # Specs
16
+ #
17
+
18
+ it "converts TIMESTAMP values to Time objects" do
19
+ result = connection.query( %{RETURN timestamp("1970-01-01 00:00:00.004666-10") as x;} )
20
+
21
+ expect( result ).to be_a( Kuzu::Result )
22
+ expect( result ).to be_success
23
+
24
+ value = result.first
25
+ expect( value ).to include( 'x' )
26
+
27
+ x = value['x']
28
+ expect( x ).to be_a( Time )
29
+ expect( x.year ).to eq( 1970 )
30
+ expect( x.month ).to eq( 1 )
31
+ expect( x.day ).to eq( 1 )
32
+
33
+ result.finish
34
+ end
35
+
36
+
37
+ it "converts STRUCT values to OpenStructs" do
38
+ result = connection.query( "RETURN {first: 'Adam', last: 'Smith'} AS record;" )
39
+
40
+ expect( result ).to be_a( Kuzu::Result )
41
+ expect( result ).to be_success
42
+ Kuzu.logger.debug "Result is: %p" % [ result ]
43
+
44
+ value = result.first
45
+ expect( value ).to include( 'record' )
46
+
47
+ record = value['record']
48
+ expect( record ).to be_a( OpenStruct )
49
+ expect( record.first ).to eq( "Adam" )
50
+ expect( record.first.encoding ).to eq( Encoding::UTF_8 )
51
+ expect( record.last ).to eq( "Smith" )
52
+ expect( record.last.encoding ).to eq( Encoding::UTF_8 )
53
+
54
+ result.finish
55
+ end
56
+
57
+
58
+ it "converts MAP values to Hashes" do
59
+ result = connection.query( "RETURN map([1, 2], ['a', 'b']) AS m;" )
60
+
61
+ expect( result ).to be_a( Kuzu::Result )
62
+ expect( result ).to be_success
63
+ Kuzu.logger.debug "Result is: %p" % [ result ]
64
+
65
+ value = result.first
66
+ expect( value ).to include( 'm' )
67
+
68
+ record = value['m']
69
+ expect( record ).to be_a( Hash )
70
+ expect( record ).to eq({ 1 => 'a', 2 => 'b' })
71
+
72
+ result.finish
73
+ end
74
+
75
+
76
+ it "converts LIST values to Arrays" do
77
+ result = connection.query( 'RETURN ["Alice", "Bob"] AS l;' )
78
+
79
+ expect( result ).to be_a( Kuzu::Result )
80
+ expect( result ).to be_success
81
+ Kuzu.logger.debug "Result is: %p" % [ result ]
82
+
83
+ value = result.first
84
+ expect( value ).to include( 'l' )
85
+
86
+ record = value['l']
87
+ expect( record ).to be_an( Array )
88
+ expect( record ).to eq( ['Alice', 'Bob'] )
89
+
90
+ result.finish
91
+ end
92
+
93
+
94
+ it "converts ARRAY values to Arrays" do
95
+ result = connection.query( "RETURN CAST([3,4,12,11], 'INT64[4]') as a;" )
96
+
97
+ expect( result ).to be_a( Kuzu::Result )
98
+ expect( result ).to be_success
99
+ Kuzu.logger.debug "Result is: %p" % [ result ]
100
+
101
+ value = result.first
102
+ expect( value ).to include( 'a' )
103
+
104
+ record = value['a']
105
+ expect( record ).to be_an( Array )
106
+ expect( record ).to eq( [3, 4, 12, 11] )
107
+
108
+ result.finish
109
+ end
110
+
111
+
112
+ it "converts ARRAYs of LIST values correctly" do
113
+ result = connection.query( "RETURN CAST([[5,2,1],[2,3],[15,64,74]], 'INT64[][3]') as a;" )
114
+
115
+ expect( result ).to be_a( Kuzu::Result )
116
+ expect( result ).to be_success
117
+ Kuzu.logger.debug "Result is: %p" % [ result ]
118
+
119
+ value = result.first
120
+ expect( value ).to include( 'a' )
121
+
122
+ record = value['a']
123
+ expect( record ).to be_an( Array )
124
+ expect( record ).to eq( [ [5, 2, 1], [2, 3], [15, 64, 74] ] )
125
+
126
+ result.finish
127
+ end
128
+
129
+
130
+ it "converts NODE values to Kuzu::Node objects" do
131
+ connection.run( <<~END_OF_SCHEMA )
132
+ CREATE NODE TABLE Person(id INT64, name STRING, age INT64, PRIMARY KEY(id));
133
+ COPY Person FROM 'spec/data/test/Person.csv';
134
+ END_OF_SCHEMA
135
+ result = connection.query( "MATCH (a:Person) RETURN a;" )
136
+
137
+ expect( result ).to be_a( Kuzu::Result )
138
+ expect( result ).to be_success
139
+ expect( result.num_tuples ).to eq( 120 )
140
+
141
+ result.each do |value|
142
+ expect( value ).to include( 'a' )
143
+
144
+ node = value['a']
145
+ expect( node ).to be_a( Kuzu::Node )
146
+
147
+ expect( node.properties.keys ).to contain_exactly( :id, :name, :age )
148
+ end
149
+
150
+ result.finish
151
+ end
152
+
153
+
154
+ it "converts REL values to Kuzu::Rel objects" do
155
+ connection.run( <<~END_OF_SCHEMA )
156
+ CREATE NODE TABLE Person(id INT64, name STRING, age INT64, PRIMARY KEY(id));
157
+ CREATE REL TABLE Follows (from Person to Person, since INT64);
158
+ COPY Person FROM 'spec/data/test/Person.csv';
159
+ COPY Follows FROM 'spec/data/test/Follows.csv';
160
+ END_OF_SCHEMA
161
+ result = connection.query( "MATCH (a:Person)-[r:Follows]->(b:Person) RETURN r;" )
162
+
163
+ expect( result ).to be_a( Kuzu::Result )
164
+ expect( result ).to be_success
165
+ expect( result.num_tuples ).to eq( 5 )
166
+
167
+ result.each do |value|
168
+ expect( value ).to include( 'r' )
169
+
170
+ rel = value['r']
171
+ expect( rel ).to be_a( Kuzu::Rel )
172
+
173
+ expect( rel.src_id ).to_not be_nil
174
+ expect( rel.dst_id ).to_not be_nil
175
+
176
+ expect( rel.properties.keys ).to contain_exactly( :since )
177
+ end
178
+
179
+ result.finish
180
+ end
181
+
182
+
183
+ it "converts RECURSIVE_REL values to Kuzu::RecursiveRel objects" do
184
+ connection.run( <<~END_OF_SCHEMA )
185
+ CREATE NODE TABLE Person(id INT64, name STRING, age INT64, PRIMARY KEY(id));
186
+ CREATE REL TABLE Follows (from Person to Person, since INT64);
187
+ COPY Person FROM 'spec/data/test/Person.csv';
188
+ COPY Follows FROM 'spec/data/test/Follows.csv';
189
+ END_OF_SCHEMA
190
+ result = connection.query( <<~END_OF_QUERY )
191
+ MATCH p = (a:Person)-[:Follows]->(b:Person)
192
+ WHERE a.name = 'Jake Kling' AND b.name = 'Joaquin Schamberger'
193
+ RETURN p;
194
+ END_OF_QUERY
195
+
196
+ expect( result ).to be_a( Kuzu::Result )
197
+ expect( result ).to be_success
198
+
199
+ result.each do |value|
200
+ expect( value ).to include( 'p' )
201
+
202
+ rel = value['p']
203
+ expect( rel ).to be_a( Kuzu::RecursiveRel )
204
+
205
+ expect( rel.nodes ).to be_an( Array ).and have_attributes( length: 2 )
206
+ expect( rel.nodes[0] ).to be_a( Kuzu::Node )
207
+ expect( rel.nodes[0][:name] ).to eq( 'Jake Kling' )
208
+ expect( rel.nodes[1] ).to be_a( Kuzu::Node )
209
+ expect( rel.nodes[1][:name] ).to eq( 'Joaquin Schamberger' )
210
+
211
+ expect( rel.rels ).to be_an( Array ).and have_attributes( length: 1 )
212
+ expect( rel.rels[0][:since] ).to eq( 2012 )
213
+ end
214
+
215
+ result.finish
216
+ end
217
+
218
+
219
+ it "converts UNIONs to something"
220
+
221
+
222
+ it "converts between nil and NULL" do
223
+ stmt = connection.prepare( "RETURN $the_value AS value;" )
224
+ result = stmt.execute( the_value: nil )
225
+
226
+ expect( result.first ).to eq( {'value' => nil} )
227
+
228
+ result.finish
229
+ end
230
+
231
+
232
+ it "converts UUIDs to Strings" do
233
+ result = connection.query( "RETURN UUID('A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11') as u;" )
234
+ uuid = result.first['u']
235
+
236
+ expect( uuid ).to be_a( String )
237
+ expect( uuid ).to eq( 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' )
238
+
239
+ result.finish
240
+ end
241
+
242
+
243
+ it "converts DECIMAL types to Float objects" do
244
+ result = connection.query( %{RETURN CAST(127.3, "DECIMAL(5, 2)") AS d;} )
245
+ rval = result.first['d']
246
+
247
+ expect( rval ).to be_a( Float )
248
+ expect( rval ).to eq( 127.3 )
249
+
250
+ result.finish
251
+ end
252
+
253
+
254
+ end
data/spec/kuzu_spec.rb ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby -S rspec
2
+
3
+ require_relative 'spec_helper'
4
+ require 'kuzu'
5
+
6
+
7
+ RSpec.describe( Kuzu ) do
8
+
9
+ let( :spec_tmpdir ) do
10
+ tmpfile_pathname()
11
+ end
12
+
13
+
14
+ it "should have a VERSION constant" do
15
+ expect( subject.const_get('VERSION') ).to_not be_empty
16
+ end
17
+
18
+
19
+ it "knows what version of Kuzu it's linked with" do
20
+ expect( described_class.kuzu_version ).to match( /\A\d+\.\d+\.\d+/ )
21
+ end
22
+
23
+
24
+ it "knows what version of the storage scheme it's using" do
25
+ result = described_class.storage_version
26
+
27
+ expect( result ).to be_an( Integer )
28
+ expect( result ).to be > 0
29
+ end
30
+
31
+
32
+ it "can construct an in-memory database with reasonable defaults" do
33
+ expect( described_class.database ).to be_a( Kuzu::Database )
34
+ end
35
+
36
+
37
+ it "can construct an in-memory database with a nil path" do
38
+ expect( described_class.database(nil) ).to be_a( Kuzu::Database )
39
+ end
40
+
41
+
42
+ it "can construct an in-memory database explicitly" do
43
+ expect( described_class.database(:memory) ).to be_a( Kuzu::Database )
44
+ end
45
+
46
+
47
+ it "can construct a on-disk database with reasonable defaults" do
48
+ filename = spec_tmpdir + 'spec_db'
49
+ result = described_class.database( filename )
50
+
51
+ expect( result ).to be_a( Kuzu::Database )
52
+ expect( filename ).to be_a_directory
53
+ end
54
+
55
+ end