ruby-ladybug 0.1.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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/History.md +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +250 -0
- data/ext/ladybug_ext/config.c +318 -0
- data/ext/ladybug_ext/connection.c +331 -0
- data/ext/ladybug_ext/database.c +197 -0
- data/ext/ladybug_ext/extconf.rb +20 -0
- data/ext/ladybug_ext/ladybug_ext.c +158 -0
- data/ext/ladybug_ext/ladybug_ext.h +132 -0
- data/ext/ladybug_ext/node.c +24 -0
- data/ext/ladybug_ext/prepared_statement.c +396 -0
- data/ext/ladybug_ext/query_summary.c +140 -0
- data/ext/ladybug_ext/recursive_rel.c +24 -0
- data/ext/ladybug_ext/rel.c +24 -0
- data/ext/ladybug_ext/result.c +514 -0
- data/ext/ladybug_ext/types.c +619 -0
- data/lib/ladybug/config.rb +70 -0
- data/lib/ladybug/connection.rb +51 -0
- data/lib/ladybug/database.rb +53 -0
- data/lib/ladybug/node.rb +46 -0
- data/lib/ladybug/prepared_statement.rb +44 -0
- data/lib/ladybug/query_summary.rb +28 -0
- data/lib/ladybug/recursive_rel.rb +37 -0
- data/lib/ladybug/rel.rb +57 -0
- data/lib/ladybug/result.rb +196 -0
- data/lib/ladybug.rb +89 -0
- data/spec/ladybug/config_spec.rb +98 -0
- data/spec/ladybug/connection_spec.rb +36 -0
- data/spec/ladybug/database_spec.rb +57 -0
- data/spec/ladybug/prepared_statement_spec.rb +91 -0
- data/spec/ladybug/query_summary_spec.rb +30 -0
- data/spec/ladybug/result_spec.rb +225 -0
- data/spec/ladybug/types_spec.rb +285 -0
- data/spec/ladybug_spec.rb +83 -0
- data/spec/spec_helper.rb +101 -0
- data.tar.gz.sig +0 -0
- metadata +177 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
require 'ladybug/connection'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RSpec.describe( Ladybug::Connection ) do
|
|
9
|
+
|
|
10
|
+
let( :db ) { Ladybug.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
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
require 'ladybug/database'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RSpec.describe( Ladybug::Database ) do
|
|
9
|
+
|
|
10
|
+
let( :spec_tmpdir ) do
|
|
11
|
+
path = tmpfile_pathname()
|
|
12
|
+
path.mkpath
|
|
13
|
+
return path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let( :db_path ) { spec_tmpdir + 'spec_db' }
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
after( :each ) do
|
|
20
|
+
GC.start
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "can be created in-memory" do
|
|
24
|
+
instance = described_class.new( '' )
|
|
25
|
+
expect( instance ).to be_a( described_class )
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
it "can be created read-only from an existing on-disk database" do
|
|
30
|
+
_original = described_class.new( db_path.to_s )
|
|
31
|
+
|
|
32
|
+
ro = described_class.new( db_path.to_s, read_only: true )
|
|
33
|
+
expect( ro ).to be_read_only
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
it "can be created without auto-checkpointing" do
|
|
38
|
+
instance = described_class.new( db_path.to_s, auto_checkpoint: false )
|
|
39
|
+
expect( instance ).not_to be_auto_checkpointing
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
it "can be created without compression" do
|
|
44
|
+
instance = described_class.new( db_path.to_s, enable_compression: false )
|
|
45
|
+
expect( instance ).not_to be_compression_enabled
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
it "can create a connection to itself" do
|
|
50
|
+
instance = described_class.new( db_path.to_s, enable_compression: false )
|
|
51
|
+
|
|
52
|
+
result = instance.connect
|
|
53
|
+
|
|
54
|
+
expect( result ).to be_a( Ladybug::Connection )
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
require 'ladybug/prepared_statement'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RSpec.describe( Ladybug::PreparedStatement ) do
|
|
9
|
+
|
|
10
|
+
let( :db ) { Ladybug.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 "errors 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
|
+
expect {
|
|
53
|
+
statement.execute
|
|
54
|
+
}.to raise_error( Ladybug::QueryError, /parameter \w+ not found/i )
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
it "can be executed if all of its variables are bound" do
|
|
59
|
+
statement = described_class.new( connection, <<~END_OF_QUERY )
|
|
60
|
+
MATCH (u:User)
|
|
61
|
+
WHERE u.age >= $min_age and u.age <= $max_age
|
|
62
|
+
RETURN u.name
|
|
63
|
+
END_OF_QUERY
|
|
64
|
+
|
|
65
|
+
result = statement.execute( min_age: 40, max_age: 50 )
|
|
66
|
+
|
|
67
|
+
expect( result.num_tuples ).to eq( 2 )
|
|
68
|
+
|
|
69
|
+
result.finish
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
it "can be reused" do
|
|
74
|
+
statement = described_class.new( connection, <<~END_OF_QUERY )
|
|
75
|
+
MATCH (u:User)
|
|
76
|
+
WHERE u.age >= $min_age and u.age <= $max_age
|
|
77
|
+
RETURN u.name
|
|
78
|
+
END_OF_QUERY
|
|
79
|
+
|
|
80
|
+
result = statement.execute( min_age: 40, max_age: 50 )
|
|
81
|
+
expect( result ).to be_success
|
|
82
|
+
expect( result.num_tuples ).to eq( 2 )
|
|
83
|
+
result.finish
|
|
84
|
+
|
|
85
|
+
result = statement.execute( min_age: 5, max_age: 90 )
|
|
86
|
+
expect( result ).to be_success
|
|
87
|
+
expect( result.num_tuples ).to eq( 4 )
|
|
88
|
+
result.finish
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
require 'ladybug/query_summary'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RSpec.describe( Ladybug::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 ) { Ladybug.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,225 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
require 'ladybug/result'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RSpec.describe( Ladybug::Result ) do
|
|
9
|
+
|
|
10
|
+
let( :db ) { Ladybug.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( Ladybug::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( Ladybug::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.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 "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
|
+
|
|
127
|
+
it "can iterate over result sets" do
|
|
128
|
+
result = described_class.from_query( connection, <<~END_QUERY )
|
|
129
|
+
return 1;
|
|
130
|
+
return 2;
|
|
131
|
+
return 3;
|
|
132
|
+
END_QUERY
|
|
133
|
+
|
|
134
|
+
rval = result.each_set.flat_map do |subset|
|
|
135
|
+
subset.to_a
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
expect( rval ).to eq([
|
|
139
|
+
{ "1" => 1 },
|
|
140
|
+
{ "2" => 2 },
|
|
141
|
+
{ "3" => 3 },
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
result.finish
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
it "also finishes successive results if they've been fetched when the first one is finished" do
|
|
149
|
+
result = described_class.from_query( connection, <<~END_QUERY )
|
|
150
|
+
return 1;
|
|
151
|
+
return 2;
|
|
152
|
+
return 3;
|
|
153
|
+
END_QUERY
|
|
154
|
+
|
|
155
|
+
second_result = result.next_set
|
|
156
|
+
|
|
157
|
+
result.finish
|
|
158
|
+
|
|
159
|
+
expect( second_result ).to be_finished
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
it "errors if it's used after being finished" do
|
|
164
|
+
result = described_class.from_query( connection, "return 1;" )
|
|
165
|
+
result.finish
|
|
166
|
+
|
|
167
|
+
expect( result ).to be_finished
|
|
168
|
+
|
|
169
|
+
expect {
|
|
170
|
+
result.get_next_values
|
|
171
|
+
}.to raise_error( Ladybug::FinishedError )
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
describe "prepared statement constructor" do
|
|
178
|
+
|
|
179
|
+
before( :each ) do
|
|
180
|
+
setup_demo_db()
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
it "can be created via a prepared statement" do
|
|
185
|
+
statement = Ladybug::PreparedStatement.new( connection, <<~END_OF_QUERY )
|
|
186
|
+
MATCH (u:User)
|
|
187
|
+
WHERE u.age > $min_age and u.age < $max_age
|
|
188
|
+
RETURN u.name
|
|
189
|
+
END_OF_QUERY
|
|
190
|
+
statement.bind( min_age: 32, max_age: 46 )
|
|
191
|
+
|
|
192
|
+
result = described_class.from_prepared_statement( statement )
|
|
193
|
+
|
|
194
|
+
expect( result ).to be_a( described_class )
|
|
195
|
+
expect( result ).to be_success
|
|
196
|
+
|
|
197
|
+
result.finish
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
it "is automatically finished if constructed with a block" do
|
|
202
|
+
statement = Ladybug::PreparedStatement.new( connection, <<~END_OF_QUERY )
|
|
203
|
+
MATCH (u:User)
|
|
204
|
+
WHERE u.age > $min_age and u.age < $max_age
|
|
205
|
+
RETURN u.name
|
|
206
|
+
END_OF_QUERY
|
|
207
|
+
statement.bind( min_age: 32, max_age: 46 )
|
|
208
|
+
|
|
209
|
+
block_result = nil
|
|
210
|
+
rval = described_class.from_prepared_statement( statement ) do |result|
|
|
211
|
+
block_result = result
|
|
212
|
+
expect( result ).to be_a( described_class )
|
|
213
|
+
expect( result ).to be_success
|
|
214
|
+
|
|
215
|
+
result.first['u.name']
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
expect( block_result ).to be_finished
|
|
219
|
+
expect( rval ).to eq( "Karissa" )
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
end
|