questdb 0.1.0-arm64-darwin

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/LICENSE.txt +201 -0
  4. data/README.md +170 -0
  5. data/SECURITY.md +19 -0
  6. data/THIRD_PARTY_NOTICES.md +242 -0
  7. data/docs/arrow.md +89 -0
  8. data/docs/configuration.md +61 -0
  9. data/docs/design.md +46 -0
  10. data/docs/ingestion.md +96 -0
  11. data/docs/performance.md +36 -0
  12. data/docs/python-parity.md +34 -0
  13. data/docs/querying.md +54 -0
  14. data/docs/release-infrastructure.md +70 -0
  15. data/docs/releasing.md +47 -0
  16. data/docs/troubleshooting.md +41 -0
  17. data/examples/basic.rb +32 -0
  18. data/examples/callbacks.rb +46 -0
  19. data/examples/columnar.rb +41 -0
  20. data/examples/independent_buffer.rb +39 -0
  21. data/examples/query.rb +28 -0
  22. data/examples/standalone.rb +33 -0
  23. data/examples/tls_auth.rb +33 -0
  24. data/lib/questdb/arrow.rb +250 -0
  25. data/lib/questdb/client.rb +308 -0
  26. data/lib/questdb/column_vector.rb +91 -0
  27. data/lib/questdb/config.rb +161 -0
  28. data/lib/questdb/connection_event.rb +60 -0
  29. data/lib/questdb/direct_ingestion.rb +440 -0
  30. data/lib/questdb/event_inbox.rb +60 -0
  31. data/lib/questdb/ingress/buffer.rb +104 -0
  32. data/lib/questdb/ingress/error.rb +43 -0
  33. data/lib/questdb/ingress/float64_array.rb +60 -0
  34. data/lib/questdb/ingress/native.rb +40 -0
  35. data/lib/questdb/ingress/timestamp.rb +53 -0
  36. data/lib/questdb/ingress.rb +77 -0
  37. data/lib/questdb/native/3.1/questdb_ingress.bundle +0 -0
  38. data/lib/questdb/native/3.2/questdb_ingress.bundle +0 -0
  39. data/lib/questdb/native/3.3/questdb_ingress.bundle +0 -0
  40. data/lib/questdb/native/3.4/questdb_ingress.bundle +0 -0
  41. data/lib/questdb/pooled_sender.rb +215 -0
  42. data/lib/questdb/query.rb +429 -0
  43. data/lib/questdb/sender.rb +436 -0
  44. data/lib/questdb/sender_error.rb +49 -0
  45. data/lib/questdb/types.rb +198 -0
  46. data/lib/questdb/version.rb +27 -0
  47. data/lib/questdb.rb +22 -0
  48. data/questdb.gemspec +69 -0
  49. data/sig/questdb-arrow.rbs +46 -0
  50. data/sig/questdb.rbs +337 -0
  51. metadata +188 -0
@@ -0,0 +1,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ___ _ ____ ____
4
+ # / _ \ _ _ ___ ___| |_| _ \| __ )
5
+ # | | | | | | |/ _ \/ __| __| | | | _ \
6
+ # | |_| | |_| | __/\__ \ |_| |_| | |_) |
7
+ # \__\_\\__,_|\___||___/\__|____/|____/
8
+ #
9
+ # Copyright (c) 2014-2019 Appsicle
10
+ # Copyright (c) 2019-2026 QuestDB
11
+ #
12
+ # Licensed under the Apache License, Version 2.0 (the "License");
13
+ # you may not use this file except in compliance with the License.
14
+ # You may obtain a copy of the License at
15
+ #
16
+ # http://www.apache.org/licenses/LICENSE-2.0
17
+ #
18
+ # Unless required by applicable law or agreed to in writing, software
19
+ # distributed under the License is distributed on an "AS IS" BASIS,
20
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ # See the License for the specific language governing permissions and
22
+ # limitations under the License.
23
+ #
24
+
25
+ module QuestDB
26
+ Column = Struct.new(:name, :kind, :metadata, keyword_init: true) do
27
+ def initialize(name:, kind:, metadata: {})
28
+ super(name: String(name).dup.freeze, kind: kind.to_sym, metadata: metadata.dup.freeze)
29
+ freeze
30
+ end
31
+ end
32
+
33
+ class Row
34
+ attr_reader :names, :values
35
+ def initialize(names, values, name_index = nil)
36
+ @names = names
37
+ @values = values.freeze
38
+ @name_index = name_index || begin
39
+ index = {}
40
+ names.each_with_index { |name, offset| index[name] ||= offset }
41
+ index.freeze
42
+ end
43
+ freeze
44
+ end
45
+ def [](key)
46
+ return values.fetch(key) if key.is_a?(Integer)
47
+ index = @name_index[row_key(key)]
48
+ index ? values[index] : nil
49
+ end
50
+ def fetch(key, *defaults, &block)
51
+ return values.fetch(key, *defaults, &block) if key.is_a?(Integer)
52
+ raise ArgumentError, "wrong number of default values" if defaults.length > 1
53
+ index = @name_index[row_key(key)]
54
+ return values[index] if index
55
+ return yield(key) if block
56
+ return defaults.first unless defaults.empty?
57
+ raise KeyError, "column not found: #{key.inspect}"
58
+ end
59
+ def to_a = values.dup
60
+ def to_h
61
+ names.each_with_index.each_with_object({}) { |(name, index), result| result[name] = values[index] }
62
+ end
63
+ def deconstruct = values
64
+
65
+ private
66
+
67
+ def row_key(key) = key.is_a?(Symbol) ? key.name : key.to_s
68
+ end
69
+
70
+ class ResultBatch
71
+ attr_reader :columns, :rows
72
+ def self.columns_from_native(native_columns)
73
+ native_columns.map do |name, kind, metadata|
74
+ Column.new(name: name, kind: kind, metadata: metadata || {})
75
+ end.freeze
76
+ end
77
+
78
+ def initialize(native_columns, native_rows)
79
+ @columns = self.class.columns_from_native(native_columns)
80
+ names = @columns.map(&:name).freeze
81
+ name_index = {}
82
+ names.each_with_index { |name, index| name_index[name] ||= index }
83
+ name_index.freeze
84
+ @rows = native_rows.map { |values| Row.new(names, values, name_index) }.freeze
85
+ freeze
86
+ end
87
+ def row_count = rows.length
88
+ def column_count = columns.length
89
+ def empty? = rows.empty?
90
+ end
91
+
92
+ class QueryResult
93
+ include Enumerable
94
+ attr_reader :columns
95
+
96
+ def initialize(reader, native)
97
+ @reader = reader
98
+ @native = native
99
+ @owner_thread = Thread.current
100
+ @columns = nil
101
+ @closed = false
102
+ @consumed = false
103
+ @cancelled = false
104
+ @close_reader = false
105
+ end
106
+
107
+ def each_batch
108
+ return enum_for(__method__) unless block_given?
109
+ each_native_batch do |native_columns, native_rows|
110
+ batch = ResultBatch.new(native_columns, native_rows)
111
+ @columns ||= batch.columns
112
+ yield batch
113
+ true
114
+ end
115
+ self
116
+ end
117
+
118
+ def each
119
+ return enum_for(__method__) unless block_given?
120
+ each_batch { |batch| batch.rows.each { |row| yield row } }
121
+ self
122
+ end
123
+
124
+ # Lower-allocation streaming for callers that only need positional values.
125
+ # Arrays are owned copies and frozen before they are yielded; unlike Row,
126
+ # they do not support name-based lookup.
127
+ def each_values
128
+ return enum_for(__method__) unless block_given?
129
+ each_native_batch do |native_columns, native_rows|
130
+ @columns ||= ResultBatch.columns_from_native(native_columns)
131
+ native_rows.each do |values|
132
+ values.freeze
133
+ yield values
134
+ end
135
+ !native_rows.empty?
136
+ end
137
+ self
138
+ end
139
+
140
+ # Whole-result materialization can safely authorize native replay: if a
141
+ # failover reset occurs, discard the partial prefix before accepting the
142
+ # replayed batch. Streaming iteration cannot retract yielded rows and raises
143
+ # instead (see #each_batch).
144
+ def to_a
145
+ start_consumption!
146
+ rows = []
147
+ reset_sequence = 0
148
+ begin
149
+ while (payload = @native.next_batch)
150
+ native_columns, native_rows, sequence = payload
151
+ if sequence != reset_sequence
152
+ rows.clear
153
+ @columns = nil
154
+ reset_sequence = sequence
155
+ end
156
+ batch = ResultBatch.new(native_columns, native_rows)
157
+ @columns ||= batch.columns
158
+ rows.concat(batch.rows)
159
+ end
160
+ ensure
161
+ close_preserving_primary_error
162
+ end
163
+ rows
164
+ end
165
+
166
+ def cancel
167
+ return self if @closed || @cancelled
168
+ ensure_thread!
169
+ begin
170
+ @native.cancel
171
+ rescue Exception => error
172
+ begin
173
+ close
174
+ rescue Exception
175
+ # Preserve the cancellation error; native close/drop is best-effort.
176
+ end
177
+ raise error
178
+ end
179
+ @cancelled = true
180
+ self
181
+ end
182
+
183
+ def close
184
+ return self if @closed
185
+ ensure_thread!
186
+ reusable = @native.close
187
+ @closed = true
188
+ @reader.send(:result_released, self, reusable: reusable)
189
+ @reader.close if @close_reader && !@reader.closed?
190
+ self
191
+ end
192
+
193
+ def closed? = @closed || @native.closed?
194
+ def consumed? = @consumed
195
+
196
+ def open
197
+ raise ArgumentError, "a block is required" unless block_given?
198
+ begin
199
+ yield self
200
+ ensure
201
+ close
202
+ end
203
+ end
204
+
205
+ def initialize_copy(*) = raise(TypeError, "QuestDB query results cannot be copied")
206
+
207
+ protected
208
+
209
+ def close_reader_on_close!
210
+ @close_reader = true
211
+ self
212
+ end
213
+
214
+ private
215
+
216
+ def each_native_batch
217
+ start_consumption!
218
+ reset_sequence = 0
219
+ yielded = false
220
+ begin
221
+ while (payload = @native.next_batch)
222
+ native_columns, native_rows, sequence = payload
223
+ if yielded && sequence != reset_sequence
224
+ raise Ingress::IngressError.new(
225
+ "query failed over after rows were delivered; streaming replay would duplicate results",
226
+ code: :failover_would_duplicate,
227
+ in_doubt: true
228
+ )
229
+ end
230
+ reset_sequence = sequence
231
+ delivered = yield native_columns, native_rows
232
+ yielded ||= delivered
233
+ end
234
+ ensure
235
+ close_preserving_primary_error
236
+ end
237
+ end
238
+
239
+ def start_consumption!
240
+ ensure_thread!
241
+ raise Ingress::IngressError.new("query result is single-use", code: :invalid_api_call) if @consumed
242
+ raise Ingress::IngressError.new("query result is closed", code: :invalid_api_call) if closed?
243
+ @consumed = true
244
+ end
245
+
246
+ def close_preserving_primary_error
247
+ primary = $!
248
+ begin
249
+ close
250
+ rescue Exception
251
+ raise unless primary
252
+ end
253
+ end
254
+
255
+ def ensure_thread!
256
+ return if Thread.current.equal?(@owner_thread)
257
+ raise Ingress::IngressError.new("query result must be used on the thread that created it", code: :invalid_api_call)
258
+ end
259
+ end
260
+
261
+ class PooledReader
262
+ def initialize(client, native)
263
+ @client = client
264
+ @native = native
265
+ @owner_thread = Thread.current
266
+ @active_result = nil
267
+ @closed = false
268
+ end
269
+
270
+ def query(sql, binds: nil, reset_symbol_dict: true)
271
+ ensure_open!
272
+ raise Ingress::IngressError.new("reader already has an active result", code: :invalid_api_call) if @active_result && !@active_result.closed?
273
+ descriptors = Bindings.normalize(binds)
274
+ result = QueryResult.new(self, @native.query(String(sql), descriptors, !!reset_symbol_dict))
275
+ @active_result = result
276
+ return result unless block_given?
277
+ begin
278
+ yield result
279
+ ensure
280
+ result.close
281
+ end
282
+ end
283
+
284
+ def execute(sql, binds: nil)
285
+ query(sql, binds: binds) { |result| result.each_batch { |_batch| } }
286
+ nil
287
+ end
288
+
289
+ def server_info
290
+ ensure_open!
291
+ raise Ingress::IngressError.new("reader has an active result", code: :invalid_api_call) if @active_result && !@active_result.closed?
292
+ ServerInfo.from_native(@native.server_info)
293
+ end
294
+
295
+ def close
296
+ return self if @closed
297
+ ensure_thread!
298
+ @active_result.close if @active_result && !@active_result.closed?
299
+ return self if @closed
300
+ @native.close(false)
301
+ @closed = true
302
+ self
303
+ end
304
+
305
+ def closed? = @closed || @native.closed?
306
+
307
+ def open
308
+ raise ArgumentError, "a block is required" unless block_given?
309
+ begin
310
+ yield self
311
+ ensure
312
+ close
313
+ end
314
+ end
315
+
316
+ def initialize_copy(*) = raise(TypeError, "QuestDB pooled readers cannot be copied")
317
+
318
+ protected
319
+
320
+ def result_released(result, reusable: true)
321
+ @active_result = nil if @active_result.equal?(result)
322
+ unless reusable || closed?
323
+ @native.close(true)
324
+ @closed = true
325
+ end
326
+ end
327
+
328
+ private
329
+
330
+ def ensure_thread!
331
+ return if Thread.current.equal?(@owner_thread)
332
+ raise Ingress::IngressError.new("pooled reader must be used on the thread that borrowed it", code: :invalid_api_call)
333
+ end
334
+
335
+ def ensure_open!
336
+ ensure_thread!
337
+ raise Ingress::IngressError.new("pooled reader is closed", code: :invalid_api_call) if closed?
338
+ end
339
+ end
340
+
341
+ ServerInfo = Struct.new(:role, :raw_role, :epoch, :capabilities, :server_wall_ns, :cluster_id, :node_id, :zone_id, keyword_init: true) do
342
+ ROLES = %i[standalone primary replica primary_catchup other].freeze
343
+ def self.from_native(values)
344
+ new(role: values[0], raw_role: values[1], epoch: values[2], capabilities: values[3],
345
+ server_wall_ns: values[4], cluster_id: values[5], node_id: values[6], zone_id: values[7])
346
+ end
347
+ def initialize(role:, raw_role:, epoch:, capabilities:, server_wall_ns:, cluster_id:, node_id:, zone_id:)
348
+ super(role: role.to_sym, raw_role: Integer(raw_role), epoch: Integer(epoch), capabilities: Integer(capabilities),
349
+ server_wall_ns: Integer(server_wall_ns), cluster_id: String(cluster_id).dup.freeze,
350
+ node_id: String(node_id).dup.freeze, zone_id: zone_id&.then { |v| String(v).dup.freeze })
351
+ freeze
352
+ end
353
+ end
354
+
355
+ module Bindings
356
+ KIND_CODES = {
357
+ boolean: 0x01, byte: 0x02, short: 0x03, int: 0x04, long: 0x05,
358
+ float: 0x06, double: 0x07, symbol: 0x09, timestamp: 0x0a,
359
+ date: 0x0b, uuid: 0x0c, long256: 0x0d, geohash: 0x0e,
360
+ varchar: 0x0f, timestamp_nanos: 0x10, decimal64: 0x13,
361
+ decimal128: 0x14, decimal256: 0x15, char: 0x16, binary: 0x17, ipv4: 0x18
362
+ }.freeze
363
+ module_function
364
+
365
+ def normalize(binds)
366
+ return [] if binds.nil?
367
+ raise TypeError, "binds must be an Array" unless binds.is_a?(Array)
368
+ binds.each_with_index.map { |value, index| normalize_one(value, index + 1) }
369
+ end
370
+
371
+ def normalize_one(value, position)
372
+ case value
373
+ when nil then [:null, KIND_CODES[:varchar], nil, nil]
374
+ when true, false then [:bool, value]
375
+ when Integer
376
+ range_i64(value, position)
377
+ [:i64, value]
378
+ when Float then [:f64, value]
379
+ when String then [:varchar, utf8(value)]
380
+ when Time then [:ts_micros, TimestampMicros.from_time(value).value]
381
+ when TimestampNanos then [:ts_nanos, value.value]
382
+ when TimestampMicros then [:ts_micros, value.value]
383
+ when DateMillis then [:date, value.value]
384
+ when Char then [:char, value.value]
385
+ when UUID then [:uuid, value.bytes]
386
+ when Binary then [:binary, value.bytes]
387
+ when IPv4 then [:ipv4, value.value]
388
+ when IPAddr then [:ipv4, IPv4.new(value).value]
389
+ when Geohash then [:geohash, value.value, value.precision_bits]
390
+ when Long256 then [:long256, value.bytes]
391
+ when DecimalValue then decimal_descriptor(value)
392
+ when BigDecimal then decimal_descriptor(DecimalValue.from_bigdecimal(value))
393
+ when TypedNull
394
+ code = KIND_CODES.fetch(value.kind)
395
+ [:null, code, value.scale, value.precision_bits]
396
+ else
397
+ raise TypeError, "unsupported bind $#{position}: #{value.class}"
398
+ end
399
+ end
400
+
401
+ def decimal_descriptor(value)
402
+ mantissa = value.mantissa
403
+ scale = value.scale
404
+ if mantissa.between?(-(1 << 63), (1 << 63) - 1)
405
+ [:decimal64, mantissa, scale]
406
+ elsif mantissa.between?(-(1 << 127), (1 << 127) - 1)
407
+ unsigned = mantissa % (1 << 128)
408
+ high = unsigned >> 64
409
+ high -= 1 << 64 if high >= 1 << 63
410
+ [:decimal128, unsigned & 0xffff_ffff_ffff_ffff, high, scale]
411
+ elsif mantissa.between?(-(1 << 255), (1 << 255) - 1)
412
+ unsigned = mantissa % (1 << 256)
413
+ [:decimal256, [unsigned.to_s(16).rjust(64, "0")].pack("H*").reverse, scale]
414
+ else
415
+ raise RangeError, "decimal mantissa exceeds signed 256-bit range"
416
+ end
417
+ end
418
+
419
+ def range_i64(value, position)
420
+ raise RangeError, "bind $#{position} is outside signed 64-bit range" unless value.between?(-(1 << 63), (1 << 63) - 1)
421
+ end
422
+
423
+ def utf8(value)
424
+ value.encode(Encoding::UTF_8)
425
+ rescue EncodingError => error
426
+ raise Ingress::IngressError.new(error.message, code: :invalid_utf8), cause: error
427
+ end
428
+ end
429
+ end