rubydb 0.1.5 → 0.1.6
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/.gitignore +4 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +296 -227
- data/Rakefile +6 -1
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +50 -42
- data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
- data/docs/README.md +3 -1
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/cli.md +24 -0
- data/docs/contributing/benchmarking.md +16 -0
- data/docs/developer/local-development.md +32 -0
- data/docs/release.md +2 -2
- data/lessons/02-local-development.md +2 -2
- data/lessons/04-rails-complex-apps.md +2 -2
- data/lessons/05-rubydb-production-server.md +2 -2
- data/lessons/07-hybrid-microservices.md +175 -90
- data/lessons/10-release-readiness.md +184 -117
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/cli/application.rb +6 -1
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/doctor.rb +3 -0
- data/lib/rubydb/client/client.rb +7 -0
- data/lib/rubydb/client/connection.rb +15 -0
- data/lib/rubydb/client/result.rb +5 -1
- data/lib/rubydb/configuration/defaults.rb +12 -0
- data/lib/rubydb/configuration/validation.rb +8 -1
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/executor.rb +373 -11
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/planner.rb +12 -46
- data/lib/rubydb/execution/sort_executor.rb +22 -8
- data/lib/rubydb/indexes/btree.rb +31 -2
- data/lib/rubydb/rubydb.rb +7 -1
- data/lib/rubydb/server/session.rb +45 -0
- data/lib/rubydb/storage/engine.rb +74 -12
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/version.rb +1 -1
- data/lib/rubydb/wal/archive.rb +17 -0
- data/lib/rubydb/wal/wal.rb +1 -0
- data/rubydb.gemspec +12 -2
- data/scripts/build_accelerator +49 -0
- data/scripts/release +34 -4
- data/scripts/replication_failover_drill +2 -2
- metadata +49 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
require "date"
|
|
6
|
+
require "json"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
module RubyDB
|
|
10
|
+
module Accelerator
|
|
11
|
+
# High-level operations that are safe to delegate. The methods return nil
|
|
12
|
+
# in auto mode when no compatible binary is installed, allowing callers to
|
|
13
|
+
# execute the existing Ruby path without changing semantics.
|
|
14
|
+
class Client
|
|
15
|
+
attr_reader :manager
|
|
16
|
+
|
|
17
|
+
def initialize(config = {})
|
|
18
|
+
@manager = Manager.new(config || {})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def available?
|
|
22
|
+
@manager.available?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enabled?
|
|
26
|
+
@manager.enabled?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def min_rows
|
|
30
|
+
@manager.min_rows
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def read_pipeline?
|
|
34
|
+
return false if @manager.mode == "off"
|
|
35
|
+
return true if @manager.mode == "required"
|
|
36
|
+
|
|
37
|
+
@manager.read_pipeline == "on" && @manager.available?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stats
|
|
41
|
+
@manager.stats
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def restart
|
|
45
|
+
@manager.restart
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ping
|
|
49
|
+
@manager.request("ping")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sha256(data)
|
|
53
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
54
|
+
payload = @manager.request("sha256", {data_base64: Base64.strict_encode64(data.to_s.b)})
|
|
55
|
+
@manager.observe_performance(:checksum, milliseconds: elapsed_ms(started), rows: data.to_s.bytesize)
|
|
56
|
+
payload&.fetch("digest")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gzip(data, level: nil)
|
|
60
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
61
|
+
options = {data_base64: Base64.strict_encode64(data.to_s.b)}
|
|
62
|
+
options[:level] = level if level
|
|
63
|
+
payload = @manager.request("gzip", options)
|
|
64
|
+
@manager.observe_performance(:compression, milliseconds: elapsed_ms(started), rows: data.to_s.bytesize)
|
|
65
|
+
payload && Base64.strict_decode64(payload.fetch("data_base64"))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def gunzip(data)
|
|
69
|
+
payload = @manager.request("gunzip", {data_base64: Base64.strict_encode64(data.to_s.b)})
|
|
70
|
+
payload && Base64.strict_decode64(payload.fetch("data_base64"))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def rows_pipeline(rows, filters: [], order_by: [], group_by: [], aggregates: [], having: [],
|
|
74
|
+
projection: [], distinct: false, distinct_columns: [], windows: [], batch_size: nil, limit: nil, offset: 0)
|
|
75
|
+
if rows.length > @manager.max_rows
|
|
76
|
+
raise Error.new("accelerator row limit exceeded", code: "resource_limit") if @manager.mode == "required"
|
|
77
|
+
|
|
78
|
+
return nil
|
|
79
|
+
end
|
|
80
|
+
if read_pipeline? && binary_rows_safe?(rows)
|
|
81
|
+
return binary_rows_pipeline(rows, filters: filters, order_by: order_by, group_by: group_by,
|
|
82
|
+
aggregates: aggregates, having: having, projection: projection, distinct: distinct,
|
|
83
|
+
distinct_columns: distinct_columns, windows: windows, batch_size: batch_size,
|
|
84
|
+
limit: limit, offset: offset)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
json_rows_pipeline(rows, filters: filters, order_by: order_by, group_by: group_by,
|
|
88
|
+
aggregates: aggregates, having: having, projection: projection, distinct: distinct,
|
|
89
|
+
distinct_columns: distinct_columns, windows: windows, batch_size: batch_size,
|
|
90
|
+
limit: limit, offset: offset)
|
|
91
|
+
rescue RubyDB::Accelerator::Error
|
|
92
|
+
raise if @manager.mode == "required"
|
|
93
|
+
|
|
94
|
+
json_rows_pipeline(rows, filters: filters, order_by: order_by, group_by: group_by,
|
|
95
|
+
aggregates: aggregates, limit: limit, offset: offset)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def binary_rows_pipeline(rows, filters:, order_by:, group_by:, aggregates:, having:, projection:, distinct:, distinct_columns:, windows:, batch_size:, limit:, offset:)
|
|
99
|
+
specification = JSON.generate(
|
|
100
|
+
filters: filters,
|
|
101
|
+
order_by: order_by,
|
|
102
|
+
group_by: group_by,
|
|
103
|
+
aggregates: aggregates,
|
|
104
|
+
having: having,
|
|
105
|
+
projection: projection,
|
|
106
|
+
distinct: distinct,
|
|
107
|
+
distinct_columns: distinct_columns,
|
|
108
|
+
windows: windows,
|
|
109
|
+
batch_size: batch_size,
|
|
110
|
+
offset: offset,
|
|
111
|
+
limit: limit
|
|
112
|
+
)
|
|
113
|
+
payload = [specification.bytesize].pack("V") + specification.b + encode_columnar_rows(rows)
|
|
114
|
+
response = @manager.binary_request("rows_pipeline", payload)
|
|
115
|
+
decode_binary_result(response)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def json_rows_pipeline(rows, filters:, order_by:, group_by:, aggregates:, having:, projection:, distinct:, distinct_columns:, windows:, batch_size:, limit:, offset:)
|
|
119
|
+
payload = {
|
|
120
|
+
rows: rows,
|
|
121
|
+
filters: filters,
|
|
122
|
+
order_by: order_by,
|
|
123
|
+
group_by: group_by,
|
|
124
|
+
aggregates: aggregates,
|
|
125
|
+
having: having,
|
|
126
|
+
projection: projection,
|
|
127
|
+
distinct: distinct,
|
|
128
|
+
distinct_columns: distinct_columns,
|
|
129
|
+
windows: windows,
|
|
130
|
+
batch_size: batch_size,
|
|
131
|
+
offset: offset
|
|
132
|
+
}
|
|
133
|
+
payload[:limit] = limit unless limit.nil?
|
|
134
|
+
response = @manager.request("rows_pipeline", payload)
|
|
135
|
+
response&.transform_keys(&:to_sym)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def rows_pipeline_stream(rows, filters: [], order_by: [], group_by: [], aggregates: [], having: [],
|
|
139
|
+
projection: [], distinct: false, distinct_columns: [], windows: [], batch_size: 1024, limit: nil, offset: 0)
|
|
140
|
+
if rows.length > @manager.max_rows
|
|
141
|
+
raise Error.new("accelerator row limit exceeded", code: "resource_limit") if @manager.mode == "required"
|
|
142
|
+
|
|
143
|
+
return nil
|
|
144
|
+
end
|
|
145
|
+
payload = {
|
|
146
|
+
rows: rows,
|
|
147
|
+
filters: filters,
|
|
148
|
+
order_by: order_by,
|
|
149
|
+
group_by: group_by,
|
|
150
|
+
aggregates: aggregates,
|
|
151
|
+
having: having,
|
|
152
|
+
projection: projection,
|
|
153
|
+
distinct: distinct,
|
|
154
|
+
distinct_columns: distinct_columns,
|
|
155
|
+
windows: windows,
|
|
156
|
+
batch_size: batch_size,
|
|
157
|
+
offset: offset
|
|
158
|
+
}
|
|
159
|
+
payload[:limit] = limit unless limit.nil?
|
|
160
|
+
@manager.request_stream("rows_pipeline_stream", payload)&.transform_keys(&:to_sym)
|
|
161
|
+
rescue RubyDB::Accelerator::Error
|
|
162
|
+
raise if @manager.mode == "required"
|
|
163
|
+
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def wal_batch(records, compress: false)
|
|
168
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
169
|
+
payload = {
|
|
170
|
+
records: Array(records).map do |record|
|
|
171
|
+
record = record.transform_keys(&:to_sym)
|
|
172
|
+
{
|
|
173
|
+
kind: Integer(record.fetch(:kind)),
|
|
174
|
+
lsn: Integer(record.fetch(:lsn)),
|
|
175
|
+
transaction: Integer(record.fetch(:transaction, 0)),
|
|
176
|
+
payload_base64: Base64.strict_encode64(record.fetch(:payload).to_s.b)
|
|
177
|
+
}
|
|
178
|
+
end,
|
|
179
|
+
compress: compress
|
|
180
|
+
}
|
|
181
|
+
result = @manager.request("wal_batch", payload)
|
|
182
|
+
@manager.observe_performance(:wal_batch, milliseconds: elapsed_ms(started), rows: records.length)
|
|
183
|
+
result&.transform_keys(&:to_sym)
|
|
184
|
+
rescue KeyError, TypeError, ArgumentError => error
|
|
185
|
+
raise Error, "Invalid WAL batch: #{error.message}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def hash_join(left_rows, right_rows, left_key:, right_key:, join_type: "inner")
|
|
189
|
+
return nil unless read_pipeline? && binary_rows_safe?(left_rows) && binary_rows_safe?(right_rows)
|
|
190
|
+
|
|
191
|
+
specification = JSON.generate(left_key: left_key.to_s, right_key: right_key.to_s, join_type: join_type.to_s, algorithm: "hash")
|
|
192
|
+
left_batch = encode_columnar_rows(left_rows)
|
|
193
|
+
right_batch = encode_columnar_rows(right_rows)
|
|
194
|
+
payload = [specification.bytesize].pack("V") + specification.b +
|
|
195
|
+
[left_batch.bytesize].pack("V") + left_batch +
|
|
196
|
+
[right_batch.bytesize].pack("V") + right_batch
|
|
197
|
+
decode_binary_result(@manager.binary_request("hash_join", payload))
|
|
198
|
+
rescue RubyDB::Accelerator::Error
|
|
199
|
+
raise if @manager.mode == "required"
|
|
200
|
+
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def merge_join(left_rows, right_rows, left_key:, right_key:, join_type: "inner")
|
|
205
|
+
return nil unless read_pipeline? && binary_rows_safe?(left_rows) && binary_rows_safe?(right_rows)
|
|
206
|
+
return nil if left_rows.length + right_rows.length > @manager.max_rows
|
|
207
|
+
|
|
208
|
+
specification = JSON.generate(left_key: left_key.to_s, right_key: right_key.to_s,
|
|
209
|
+
join_type: join_type.to_s, algorithm: "merge")
|
|
210
|
+
left_batch = encode_columnar_rows(left_rows)
|
|
211
|
+
right_batch = encode_columnar_rows(right_rows)
|
|
212
|
+
payload = [specification.bytesize].pack("V") + specification.b +
|
|
213
|
+
[left_batch.bytesize].pack("V") + left_batch +
|
|
214
|
+
[right_batch.bytesize].pack("V") + right_batch
|
|
215
|
+
decode_binary_result(@manager.binary_request("hash_join", payload))
|
|
216
|
+
rescue RubyDB::Accelerator::Error
|
|
217
|
+
raise if @manager.mode == "required"
|
|
218
|
+
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Scan an immutable storage snapshot. The request contains no Ruby row
|
|
223
|
+
# hashes; Go opens the snapshot file, decodes pages, applies predicates,
|
|
224
|
+
# and returns a bounded columnar result.
|
|
225
|
+
def snapshot_scan(manifest, table:, columns:, column_types:, filters: [], order_by: [], index_name: nil,
|
|
226
|
+
distinct: false, distinct_columns: [], limit: nil, offset: 0, batch_size: nil)
|
|
227
|
+
return nil unless read_pipeline?
|
|
228
|
+
|
|
229
|
+
specification = {
|
|
230
|
+
snapshot: manifest,
|
|
231
|
+
table: table.to_s,
|
|
232
|
+
columns: Array(columns).map(&:to_s),
|
|
233
|
+
column_types: column_types.transform_keys(&:to_s).transform_values(&:to_s),
|
|
234
|
+
filters: filters,
|
|
235
|
+
order_by: order_by,
|
|
236
|
+
distinct: distinct,
|
|
237
|
+
distinct_columns: distinct_columns,
|
|
238
|
+
limit: limit,
|
|
239
|
+
offset: offset,
|
|
240
|
+
batch_size: batch_size,
|
|
241
|
+
max_rows: @manager.max_rows
|
|
242
|
+
}
|
|
243
|
+
specification[:index_name] = index_name.to_s if index_name
|
|
244
|
+
response = @manager.binary_request("snapshot_scan", JSON.generate(specification))
|
|
245
|
+
decode_binary_result(response, column_types: column_types)
|
|
246
|
+
rescue RubyDB::Accelerator::Error
|
|
247
|
+
raise if @manager.mode == "required"
|
|
248
|
+
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def binary_rows_safe?(rows)
|
|
253
|
+
rows.all? do |row|
|
|
254
|
+
row.all? { |_key, value| binary_value_safe?(value) }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def binary_value_safe?(value)
|
|
259
|
+
case value
|
|
260
|
+
when NilClass, TrueClass, FalseClass, String, Array, Hash
|
|
261
|
+
if value.is_a?(Array)
|
|
262
|
+
value.all? { |item| binary_value_safe?(item) }
|
|
263
|
+
elsif value.is_a?(Hash)
|
|
264
|
+
value.all? { |key, item| (key.is_a?(String) || key.is_a?(Symbol)) && binary_value_safe?(item) }
|
|
265
|
+
else
|
|
266
|
+
true
|
|
267
|
+
end
|
|
268
|
+
when Integer
|
|
269
|
+
value >= -(1 << 63) && value <= ((1 << 63) - 1)
|
|
270
|
+
when Float
|
|
271
|
+
value.finite?
|
|
272
|
+
else
|
|
273
|
+
false
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def encode_columnar_rows(rows)
|
|
278
|
+
columns = []
|
|
279
|
+
seen = {}
|
|
280
|
+
rows.each do |row|
|
|
281
|
+
row.each_key do |key|
|
|
282
|
+
name = key.to_s
|
|
283
|
+
next if seen[name]
|
|
284
|
+
|
|
285
|
+
seen[name] = true
|
|
286
|
+
columns << name
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
output = [columns.length, rows.length].pack("vV")
|
|
290
|
+
columns.each do |column|
|
|
291
|
+
bytes = column.b
|
|
292
|
+
raise Error, "RubyDB accelerator column name is too long" if bytes.bytesize > 65_535
|
|
293
|
+
|
|
294
|
+
output << [bytes.bytesize].pack("v") << bytes
|
|
295
|
+
end
|
|
296
|
+
columns.each do |column|
|
|
297
|
+
rows.each do |row|
|
|
298
|
+
value = if row.key?(column)
|
|
299
|
+
row[column]
|
|
300
|
+
elsif row.key?(column.to_sym)
|
|
301
|
+
row[column.to_sym]
|
|
302
|
+
end
|
|
303
|
+
output << encode_binary_value(value)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
output
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def encode_binary_value(value)
|
|
310
|
+
case value
|
|
311
|
+
when nil
|
|
312
|
+
[0].pack("C")
|
|
313
|
+
when true, false
|
|
314
|
+
[1, value ? 1 : 0].pack("C2")
|
|
315
|
+
when Integer
|
|
316
|
+
[2].pack("C") + [value].pack("q<")
|
|
317
|
+
when Float
|
|
318
|
+
[3].pack("C") + [value].pack("E")
|
|
319
|
+
when String
|
|
320
|
+
bytes = value.b
|
|
321
|
+
[4].pack("C") + [bytes.bytesize].pack("V") + bytes
|
|
322
|
+
when Array, Hash
|
|
323
|
+
encoded = JSON.generate(value)
|
|
324
|
+
[6].pack("C") + [encoded.bytesize].pack("V") + encoded.b
|
|
325
|
+
else
|
|
326
|
+
raise Error, "RubyDB accelerator cannot encode #{value.class} in a binary row batch"
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def decode_binary_result(data, column_types: nil)
|
|
331
|
+
offset = 0
|
|
332
|
+
metadata_length = read_uint32(data, offset)
|
|
333
|
+
offset += 4
|
|
334
|
+
metadata = JSON.parse(data.byteslice(offset, metadata_length))
|
|
335
|
+
offset += metadata_length
|
|
336
|
+
row_length = read_uint32(data, offset)
|
|
337
|
+
offset += 4
|
|
338
|
+
rows = decode_columnar_rows(data.byteslice(offset, row_length))
|
|
339
|
+
rows = restore_snapshot_types(rows, column_types || metadata["column_types"])
|
|
340
|
+
offset += row_length
|
|
341
|
+
aggregate_length = read_uint32(data, offset)
|
|
342
|
+
offset += 4
|
|
343
|
+
aggregates = decode_columnar_rows(data.byteslice(offset, aggregate_length))
|
|
344
|
+
{
|
|
345
|
+
rows: rows,
|
|
346
|
+
row_count: metadata.fetch("row_count", rows.length),
|
|
347
|
+
aggregates: metadata["has_aggregates"] ? aggregates : nil
|
|
348
|
+
}
|
|
349
|
+
rescue JSON::ParserError, TypeError, ArgumentError, RangeError => error
|
|
350
|
+
raise ProtocolError.new("Invalid RubyDB accelerator binary result: #{error.message}", code: "invalid_response")
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def restore_snapshot_types(rows, column_types)
|
|
354
|
+
return rows unless column_types.is_a?(Hash) && !column_types.empty?
|
|
355
|
+
|
|
356
|
+
rows.map do |row|
|
|
357
|
+
row.each_with_object({}) do |(column, value), restored|
|
|
358
|
+
type = column_types[column] || column_types[column.to_sym]
|
|
359
|
+
restored[column] = restore_snapshot_value(value, type)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def restore_snapshot_value(value, type)
|
|
365
|
+
return value if value.nil? || type.nil?
|
|
366
|
+
|
|
367
|
+
case type.to_s.downcase
|
|
368
|
+
when "date"
|
|
369
|
+
Date.parse(value.to_s)
|
|
370
|
+
when "time", "timestamp"
|
|
371
|
+
Time.parse(value.to_s)
|
|
372
|
+
when "decimal"
|
|
373
|
+
BigDecimal(value.to_s)
|
|
374
|
+
when "json"
|
|
375
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
|
376
|
+
else
|
|
377
|
+
value
|
|
378
|
+
end
|
|
379
|
+
rescue ArgumentError, TypeError, JSON::ParserError
|
|
380
|
+
value
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def decode_columnar_rows(data)
|
|
384
|
+
offset = 0
|
|
385
|
+
column_count = read_uint16(data, offset)
|
|
386
|
+
offset += 2
|
|
387
|
+
row_count = read_uint32(data, offset)
|
|
388
|
+
offset += 4
|
|
389
|
+
columns = column_count.times.map do
|
|
390
|
+
length = read_uint16(data, offset)
|
|
391
|
+
offset += 2
|
|
392
|
+
value = data.byteslice(offset, length).to_s
|
|
393
|
+
offset += length
|
|
394
|
+
value
|
|
395
|
+
end
|
|
396
|
+
rows = Array.new(row_count) { {} }
|
|
397
|
+
columns.each do |column|
|
|
398
|
+
row_count.times do |index|
|
|
399
|
+
value, consumed = decode_binary_value(data, offset)
|
|
400
|
+
offset += consumed
|
|
401
|
+
rows[index][column] = value
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
raise ProtocolError.new("Binary row batch has trailing bytes", code: "invalid_response") unless offset == data.bytesize
|
|
405
|
+
|
|
406
|
+
rows
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def decode_binary_value(data, offset)
|
|
410
|
+
tag = data.getbyte(offset)
|
|
411
|
+
raise RangeError, "missing binary value tag" unless tag
|
|
412
|
+
|
|
413
|
+
case tag
|
|
414
|
+
when 0 then [nil, 1]
|
|
415
|
+
when 1 then [data.getbyte(offset + 1) == 1, 2]
|
|
416
|
+
when 2 then [data.byteslice(offset + 1, 8).unpack1("q<"), 9]
|
|
417
|
+
when 3 then [data.byteslice(offset + 1, 8).unpack1("E"), 9]
|
|
418
|
+
when 4
|
|
419
|
+
length = data.byteslice(offset + 1, 4).unpack1("V")
|
|
420
|
+
[data.byteslice(offset + 5, length).to_s.force_encoding(Encoding::UTF_8), 5 + length]
|
|
421
|
+
when 5
|
|
422
|
+
length = data.byteslice(offset + 1, 4).unpack1("V")
|
|
423
|
+
[data.byteslice(offset + 5, length).to_s.b, 5 + length]
|
|
424
|
+
when 6
|
|
425
|
+
length = data.byteslice(offset + 1, 4).unpack1("V")
|
|
426
|
+
[JSON.parse(data.byteslice(offset + 5, length)), 5 + length]
|
|
427
|
+
else
|
|
428
|
+
raise RangeError, "unknown binary value tag #{tag}"
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def read_uint16(data, offset)
|
|
433
|
+
data.byteslice(offset, 2).unpack1("v")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def read_uint32(data, offset)
|
|
437
|
+
data.byteslice(offset, 4).unpack1("V")
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def close
|
|
441
|
+
@manager.close
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
def elapsed_ms(started)
|
|
447
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyDB
|
|
4
|
+
module Accelerator
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :code
|
|
7
|
+
|
|
8
|
+
def initialize(message, code: nil)
|
|
9
|
+
@code = code
|
|
10
|
+
super(message)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class UnavailableError < Error; end
|
|
15
|
+
|
|
16
|
+
class ProtocolError < Error; end
|
|
17
|
+
|
|
18
|
+
class RequestError < Error; end
|
|
19
|
+
|
|
20
|
+
class TimeoutError < Error; end
|
|
21
|
+
end
|
|
22
|
+
end
|