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.
@@ -0,0 +1,51 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'loggability'
4
+
5
+ require 'ladybug' unless defined?( Ladybug )
6
+
7
+
8
+ # Kùzu connection class
9
+ class Ladybug::Connection
10
+ extend Loggability
11
+
12
+
13
+ # Loggability API -- log to Ladybug's logger
14
+ log_to :ladybug
15
+
16
+
17
+ ### Execute the given +query_string+ via the connection and return the
18
+ ### Ladybug::Result. If a block is given, the result will instead be yielded to it,
19
+ ### finished when it returns, and the return value of the block will be returned
20
+ ### instead.
21
+ def query( query_string, &block )
22
+ result = self._query( query_string )
23
+ return Ladybug::Result.wrap_block_result( result, &block )
24
+ end
25
+
26
+
27
+ ### Create a new Ladybug::PreparedStatement for the specified +query_string+.
28
+ def prepare( query_string )
29
+ return Ladybug::PreparedStatement.new( self, query_string )
30
+ end
31
+
32
+
33
+ ### Executes the given +statement+ (a Ladybug::PreparedStatement) after binding
34
+ ### the given +bound_variables+ to it.
35
+ def execute( statement, **bound_variables, &block )
36
+ statement.bind( **bound_variables )
37
+ return statement.execute
38
+ end
39
+
40
+
41
+ ### Return a string representation of the receiver suitable for debugging.
42
+ def inspect
43
+ details = " threads:%d" % [
44
+ self.max_num_threads_for_exec,
45
+ ]
46
+
47
+ default = super
48
+ return default.sub( />/, details + '>' )
49
+ end
50
+
51
+ end # class Ladybug::Connection
@@ -0,0 +1,53 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'loggability'
4
+
5
+ require 'ladybug' unless defined?( Ladybug )
6
+
7
+
8
+ # Main Kùzu database class
9
+ class Ladybug::Database
10
+ extend Loggability
11
+
12
+
13
+ # Loggability API -- log to Ladybug's logger
14
+ log_to :ladybug
15
+
16
+
17
+ ### Return a connection to this database.
18
+ def connect
19
+ return Ladybug::Connection.new( self )
20
+ end
21
+
22
+
23
+ ### Return +true+ if this database was created in read-only mode.
24
+ def read_only?
25
+ return self.config.read_only
26
+ end
27
+
28
+
29
+ ### Returns +true+ if this database will automatically checkpoint when the size of
30
+ ### the WAL file exceeds the `checkpoint_threshold`.
31
+ def auto_checkpointing?
32
+ return self.config.auto_checkpoint
33
+ end
34
+
35
+
36
+ ### Returns +true+ if this database uses compression for data on disk.
37
+ def compression_enabled?
38
+ return self.config.enable_compression
39
+ end
40
+
41
+
42
+ ### Return a string representation of the receiver suitable for debugging.
43
+ def inspect
44
+ details = " path:%p read-only:%p" % [
45
+ self.path,
46
+ self.read_only?,
47
+ ]
48
+
49
+ default = super
50
+ return default.sub( />/, details + '>' )
51
+ end
52
+
53
+ end # class Ladybug::Database
@@ -0,0 +1,46 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'forwardable'
4
+ require 'loggability'
5
+
6
+ require 'ladybug' unless defined?( Ladybug )
7
+
8
+
9
+ # Ladybug node class
10
+ class Ladybug::Node
11
+ extend Loggability,
12
+ Forwardable
13
+
14
+ # Loggability API -- log to Ladybug's logger
15
+ log_to :ladybug
16
+
17
+
18
+ ### Create a new Node with the given +id+, +label+, and +properties+.
19
+ def initialize( id, label, **properties )
20
+ @id = id
21
+ @label = label
22
+ @properties = properties
23
+ end
24
+
25
+
26
+ ######
27
+ public
28
+ ######
29
+
30
+ ##
31
+ # The internal id value of the given node
32
+ attr_reader :id
33
+
34
+ ##
35
+ # The label value of the given node
36
+ attr_reader :label
37
+
38
+ ##
39
+ # The Hash of the Node's properties, keyed by name as a Symbol
40
+ attr_reader :properties
41
+
42
+
43
+ # Allow direct access to properties
44
+ def_delegators :@properties, :[], :[]=, :dig
45
+
46
+ end # class Ladybug::Node
@@ -0,0 +1,44 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'loggability'
4
+
5
+ require 'ladybug' unless defined?( Ladybug )
6
+
7
+
8
+ # A parameterized query which can avoid planning the same query for repeated execution
9
+ class Ladybug::PreparedStatement
10
+ extend Loggability
11
+
12
+ # Loggability API -- Use Ladybug's logger
13
+ log_to :ladybug
14
+
15
+
16
+ ### Execute the statement against its connection and return a Ladybug::Result.
17
+ ### If a +block+ is supplied, the result will be passed to it instead,
18
+ ### then finished automatically, and the return value of the block returned
19
+ ### instead.
20
+ def execute( **bound_variables, &block )
21
+ self.log.debug "Executing statement:\n%s\nwith variables:\n%p" %
22
+ [ self.query, bound_variables ]
23
+ self.bind( **bound_variables )
24
+ result = self._execute
25
+ return Ladybug::Result.wrap_block_result( result, &block )
26
+ end
27
+
28
+
29
+ ### Execute the statement against its connection and return `true` if it
30
+ ### succeeded.
31
+ def execute!( **bound_variables )
32
+ self.bind( **bound_variables )
33
+ return self._execute!
34
+ end
35
+
36
+
37
+ ### Bind the variables in the specified +variable_map+ to the statement.
38
+ def bind( **variable_map )
39
+ variable_map.each do |name, value|
40
+ self.bind_variable( name, value )
41
+ end
42
+ end
43
+
44
+ end # class Ladybug::PreparedStatement
@@ -0,0 +1,28 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'loggability'
4
+
5
+ require 'ladybug' unless defined?( Ladybug )
6
+
7
+
8
+ # Kùzu query summary class
9
+ class Ladybug::QuerySummary
10
+ extend Loggability
11
+
12
+
13
+ # Loggability API -- log to Ladybug's logger
14
+ log_to :ladybug
15
+
16
+
17
+ ### Return a string representation of the receiver suitable for debugging.
18
+ def inspect
19
+ details = " compiling: %0.3fs execution: %0.3fs" % [
20
+ self.compiling_time,
21
+ self.execution_time,
22
+ ]
23
+
24
+ default = super
25
+ return default.sub( />/, details + '>' )
26
+ end
27
+
28
+ end # class Ladybug::QuerySummary
@@ -0,0 +1,37 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'forwardable'
4
+ require 'loggability'
5
+
6
+ require 'ladybug' unless defined?( Ladybug )
7
+
8
+
9
+ # Ladybug recursive relationship class
10
+ class Ladybug::RecursiveRel
11
+ extend Loggability,
12
+ Forwardable
13
+
14
+ # Loggability API -- log to Ladybug's logger
15
+ log_to :ladybug
16
+
17
+
18
+ ### Create a new RecursiveRel with the given +nodes+ and +rels+.
19
+ def initialize( nodes, rels )
20
+ @nodes = nodes
21
+ @rels = rels
22
+ end
23
+
24
+
25
+ ######
26
+ public
27
+ ######
28
+
29
+ ##
30
+ # The Array of Ladybug::Nodes in the chain
31
+ attr_reader :nodes
32
+
33
+ ##
34
+ # The Array of Ladybug::Rels connecting the Nodes in the chain
35
+ attr_reader :rels
36
+
37
+ end # class Ladybug::RecursiveRel
@@ -0,0 +1,57 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'forwardable'
4
+ require 'loggability'
5
+
6
+ require 'ladybug' unless defined?( Ladybug )
7
+
8
+
9
+ # Ladybug rel (relationship) class
10
+ class Ladybug::Rel
11
+ extend Loggability,
12
+ Forwardable
13
+
14
+ # Loggability API -- log to Ladybug's logger
15
+ log_to :ladybug
16
+
17
+
18
+ ### Create a new Rel with the given +id+, +src_id+, +dst_id+,
19
+ ### +label+, and +properties+.
20
+ def initialize( id, src_id, dst_id, label, **properties )
21
+ @id = id
22
+ @src_id = src_id
23
+ @dst_id = dst_id
24
+ @label = label
25
+ @properties = properties
26
+ end
27
+
28
+
29
+ ######
30
+ public
31
+ ######
32
+
33
+ ##
34
+ # The internal id value of the given rel
35
+ attr_reader :id
36
+
37
+ ##
38
+ # The internal id value of the source node
39
+ attr_reader :src_id
40
+
41
+ ##
42
+ # The internal id value of the destination node
43
+ attr_reader :dst_id
44
+
45
+ ##
46
+ # The label value of the given node
47
+ attr_reader :label
48
+
49
+ ##
50
+ # The Hash of the Rel's properties, keyed by name as a Symbol
51
+ attr_reader :properties
52
+
53
+
54
+ # Allow direct access to properties
55
+ def_delegators :@properties, :[], :[]=, :dig
56
+
57
+ end # class Ladybug::Rel
@@ -0,0 +1,196 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'loggability'
4
+
5
+ require 'ladybug' unless defined?( Ladybug )
6
+
7
+
8
+ # Kùzu query result class
9
+ #
10
+ # These objects contain one result set from either a Ladybug::Connection#query call
11
+ # or Ladybug::PreparedStatement#execute. If there are multiple result sets, you can
12
+ # fetch the next one by calling Ladybug::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
+ # | Ladybug 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+ | Ladybug::Node |
44
+ # | +REL+ | Ladybug::Rel |
45
+ # | +RECURSIVE_REL+ | Ladybug::RecursiveRel |
46
+ # | +LIST+ | +Array+ |
47
+ # | +ARRAY+ | +Array+ |
48
+ #
49
+ #
50
+ class Ladybug::Result
51
+ extend Loggability
52
+
53
+
54
+ # Loggability API -- log to Ladybug's logger
55
+ log_to :ladybug
56
+
57
+
58
+ ### Execute the given +query+ via the specified +connection+ and return the
59
+ ### Ladybug::Result. If a block is given, the result will instead be yielded to it,
60
+ ### finished when it returns, and the return value of the block will be returned
61
+ ### instead.
62
+ def self::from_query( connection, query, &block )
63
+ return connection.query( query, &block )
64
+ end
65
+
66
+
67
+ ### Execute the given +statement+ and return the Ladybug::Result. If a block is given,
68
+ ### the result will instead be yielded to it, finished when it returns, and the
69
+ ### return value of the block will be returned instead.
70
+ def self::from_prepared_statement( statement, &block )
71
+ return statement.execute( &block )
72
+ end
73
+
74
+
75
+ ### If the +block+ is provided, yield +result+ to it and then call #finish on it,
76
+ ### returning the +block+ result. If +block+ is not given, just return +result+.
77
+ def self::wrap_block_result( result, &block )
78
+ return result unless block
79
+
80
+ begin
81
+ rval = block.call( result )
82
+ ensure
83
+ result.finish
84
+ end
85
+
86
+ return rval
87
+ end
88
+
89
+
90
+ ### Fetch the names of the columns in the result as an Array of Strings.
91
+ def column_names
92
+ return @column_names ||= self.get_column_names
93
+ end
94
+
95
+
96
+ ### Return a Ladybug::QuerySummary for the query that generated the Result.
97
+ def query_summary
98
+ return Ladybug::QuerySummary.from_result( self )
99
+ end
100
+
101
+
102
+ ### Get the next tuple of the result as a Hash.
103
+ def next
104
+ values = self.get_next_values or return nil
105
+ pairs = self.column_names.zip( values )
106
+ return Hash[ pairs ]
107
+ end
108
+
109
+
110
+ ### Iterate over each tuple of the result, yielding it to the +block+. If no
111
+ ### +block+ is given, return an Enumerator that will yield them instead.
112
+ def each( &block )
113
+ enum = self.tuple_enum
114
+ return enum.each( &block ) if block
115
+ return enum
116
+ end
117
+
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
+
132
+ ### Return the next result set after this one as a Ladybug::Result, or `nil`if
133
+ ### there is no next set.
134
+ def next_set
135
+ return nil unless self.has_next_set?
136
+ return self.class.from_next_set( self )
137
+ end
138
+
139
+
140
+ ### Iterate over each result set in the results, yielding it to the block. If
141
+ ### no +block+ is given, return an Enumerator tht will yield each set as its own
142
+ ### Result.
143
+ def each_set( &block )
144
+ enum = self.next_set_enum
145
+ return enum.each( &block ) if block
146
+ return enum
147
+ end
148
+
149
+
150
+ ### Return a string representation of the receiver suitable for debugging.
151
+ def inspect
152
+ if self.finished?
153
+ details = " (finished)"
154
+ else
155
+ details = " success: %p (%d tuples of %d columns)" % [
156
+ self.success?,
157
+ self.num_tuples,
158
+ self.num_columns,
159
+ ]
160
+ end
161
+
162
+ default = super
163
+ return default.sub( />/, details + '>' )
164
+ end
165
+
166
+
167
+ #########
168
+ protected
169
+ #########
170
+
171
+ ### Return an Enumerator that yields result tuples as Hashes.
172
+ def tuple_enum
173
+ self.log.debug "Fetching a tuple Enumerator"
174
+ return Enumerator.new do |yielder|
175
+ self.reset_iterator
176
+ while self.has_next?
177
+ tuple = self.next
178
+ yielder.yield( tuple )
179
+ end
180
+ end
181
+ end
182
+
183
+
184
+ ### Return an Enumerator that yields a Result for each set.
185
+ def next_set_enum
186
+ self.log.debug "Fetching a result set Enumerator"
187
+ result = self
188
+ return Enumerator.new do |yielder|
189
+ while result
190
+ yielder.yield( result )
191
+ result = result.next_set
192
+ end
193
+ end
194
+ end
195
+
196
+ end # class Ladybug::Result
data/lib/ladybug.rb ADDED
@@ -0,0 +1,89 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'ostruct'
4
+ require 'pathname'
5
+ require 'loggability'
6
+
7
+ require_relative 'ladybug_ext'
8
+
9
+ #--
10
+ # See also: ext/ladybug_ext/ladybug_ext.c
11
+ module Ladybug
12
+ extend Loggability
13
+
14
+
15
+ # Library version
16
+ VERSION = '0.1.0'
17
+
18
+
19
+ # Set up a logger for Ladybug classes
20
+ log_as :ladybug
21
+
22
+
23
+ ### Create and return a Ladybug::Database. If +path+ is +nil+, an empty string, or
24
+ ### the Symbol :memory, creates an in-memory database. Valid options are:
25
+ ###
26
+ ### `:buffer_pool_size`
27
+ ### : Max size of the buffer pool in bytes.
28
+ ###
29
+ ### `:max_num_threads`
30
+ ### : The maximum number of threads to use during query execution.
31
+ ###
32
+ ### `:enable_compression`
33
+ ### : Whether or not to compress data on-disk for supported types
34
+ ###
35
+ ### `:read_only`
36
+ ### : If true, open the database in read-only mode. No write transaction is allowed on the
37
+ ### Database object. If false, open the database read-write.
38
+ ###
39
+ ### `:max_db_size`
40
+ ### : The maximum size of the database in bytes.
41
+ ###
42
+ ### `:auto_checkpoint`
43
+ ### : If true, the database will automatically checkpoint when the size of
44
+ ### the WAL file exceeds the checkpoint threshold.
45
+ ###
46
+ ### `:checkpoint_threshold`
47
+ ### : The threshold of the WAL file size in bytes. When the size of the
48
+ ### WAL file exceeds this threshold, the database will checkpoint if
49
+ ### `auto_checkpoint` is true.
50
+ def self::database( path='', **config )
51
+ path = '' if path.nil? || path == :memory
52
+ self.log.info "Opening database %p" % [ path ]
53
+ return Ladybug::Database.new( path.to_s, **config )
54
+ end
55
+
56
+
57
+ ### Returns +true+ if the specified +pathname+ appears to be a valid Ladybug database
58
+ ### for the current version of the storage format.
59
+ def self::is_database?( pathname )
60
+ pathname = Pathname( pathname )
61
+ return false unless pathname.file?
62
+ magic = pathname.read( 5 )
63
+ return magic[0, 4] == 'LBUG' && magic[4, 1].ord == Ladybug.storage_version
64
+ end
65
+ singleton_class.alias_method( :is_ladybug_database?, :is_database? )
66
+
67
+
68
+ ### Return a Time object from the given +milliseconds+ epoch time.
69
+ def self::timestamp_from_timestamp_ms( milliseconds )
70
+ seconds, subsec = milliseconds.divmod( 1_000 )
71
+ return Time.at( seconds, subsec, :millisecond )
72
+ end
73
+
74
+
75
+ ### Return a Time object from the given +microseconds+ epoch time and
76
+ ### optional timezone offset in seconds via the +zone+ argument.
77
+ def self::timestamp_from_timestamp_us( microseconds, zone=nil )
78
+ seconds, subsec = microseconds.divmod( 1_000_000 )
79
+ return Time.at( seconds, subsec, :microsecond, in: zone )
80
+ end
81
+
82
+
83
+ ### Return a Time object from the given +nanoseconds+ epoch time.
84
+ def self::timestamp_from_timestamp_ns( nanoseconds )
85
+ seconds, subsec = nanoseconds.divmod( 1_000_000_000 )
86
+ return Time.at( seconds, subsec, :nanosecond )
87
+ end
88
+
89
+ end # module Ladybug
@@ -0,0 +1,98 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'ladybug/config'
6
+
7
+
8
+ RSpec.describe( Ladybug::Config ) do
9
+
10
+ it "can be created with defaults" do
11
+ result = described_class.new
12
+
13
+ expect( result ).to be_a( described_class )
14
+
15
+ expect( result.buffer_pool_size ).to be_a( Integer )
16
+ expect( result.max_num_threads ).to be_a( Integer )
17
+ expect( result.enable_compression ).to eq( true )
18
+ expect( result.read_only ).to eq( false )
19
+ expect( result.max_db_size ).to be_a( Integer )
20
+ expect( result.auto_checkpoint ).to eq( true )
21
+ expect( result.checkpoint_threshold ).to be_a( Integer )
22
+ end
23
+
24
+
25
+ it "can set its buffer_pool_size" do
26
+ instance = described_class.new
27
+
28
+ expect {
29
+ instance.buffer_pool_size = 2 ** 11
30
+ }.to change { instance.buffer_pool_size }.to( 2 ** 11 )
31
+ end
32
+
33
+
34
+ it "can set its max_num_threads" do
35
+ instance = described_class.new
36
+
37
+ expect {
38
+ instance.max_num_threads = 4
39
+ }.to change { instance.max_num_threads }.to( 4 )
40
+ end
41
+
42
+
43
+ it "can disable compression" do
44
+ instance = described_class.new
45
+
46
+ expect {
47
+ instance.enable_compression = false
48
+ }.to change { instance.enable_compression }.to( false )
49
+ end
50
+
51
+
52
+ it "can set read-only mode" do
53
+ instance = described_class.new
54
+
55
+ expect {
56
+ instance.read_only = true
57
+ }.to change { instance.read_only }.to( true )
58
+ end
59
+
60
+
61
+ it "can set read-only mode using a truthy value" do
62
+ instance = described_class.new
63
+
64
+ expect {
65
+ instance.read_only = :yep
66
+ }.to change { instance.read_only }.to( true )
67
+ end
68
+
69
+
70
+ it "can set its max_db_size" do
71
+ instance = described_class.new
72
+
73
+ expect {
74
+ instance.max_db_size = 2 ** 21
75
+ }.to change { instance.max_db_size }.to( 2 ** 21 )
76
+ end
77
+
78
+
79
+ it "can disable auto-checkpointing" do
80
+ instance = described_class.new
81
+
82
+ expect {
83
+ instance.auto_checkpoint = false
84
+ }.to change { instance.auto_checkpoint }.to( false )
85
+ end
86
+
87
+
88
+ it "can set its checkpoint threshold" do
89
+ instance = described_class.new
90
+
91
+ expect {
92
+ instance.checkpoint_threshold = 2 ** 22
93
+ }.to change { instance.checkpoint_threshold }.to( 2 ** 22 )
94
+ end
95
+
96
+
97
+ end
98
+