questdb 0.1.0-aarch64-linux-gnu

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.so +0 -0
  38. data/lib/questdb/native/3.2/questdb_ingress.so +0 -0
  39. data/lib/questdb/native/3.3/questdb_ingress.so +0 -0
  40. data/lib/questdb/native/3.4/questdb_ingress.so +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,436 @@
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
+ require "weakref"
25
+
26
+ module QuestDB
27
+ # A connection-level QWP/WebSocket sender. Prefer Client#sender for pooled
28
+ # production ingestion; standalone senders are useful for manual progress
29
+ # and explicit buffer control.
30
+ class Sender
31
+ PROTOCOLS = %i[ws wss].freeze
32
+ ACK_LEVELS = { ok: 0, durable: 1 }.freeze
33
+ EMPTY_HASH = {}.freeze
34
+ private_constant :EMPTY_HASH
35
+
36
+ attr_reader :protocol, :init_buf_size, :auto_flush_rows, :auto_flush_bytes, :auto_flush_interval
37
+
38
+ def initialize(protocol:, host:, port: nil, connection_listener: nil, error_handler: nil,
39
+ connection_event_inbox_capacity: 64, error_event_inbox_capacity: 64, **settings)
40
+ @protocol = protocol.to_sym
41
+ raise ArgumentError, "protocol must be :ws or :wss" unless PROTOCOLS.include?(@protocol)
42
+ host = Config.normalize_host(host)
43
+ port = Integer(port || 9000)
44
+ raise RangeError, "port must be between 1 and 65535" unless port.between?(1, 65_535)
45
+ validate_setting_keys!(settings)
46
+ effective = { "addr" => "#{host}:#{port}" }.merge(
47
+ settings.to_h { |key, value| [key.to_s, Config.setting_value(value, key)] }
48
+ )
49
+ native = effective.reject { |key, _| Config::STANDALONE_WRAPPER_KEYS.include?(key) }
50
+ @configuration = "#{@protocol}::#{native.map { |key, value| "#{key}=#{Config.encode(value, key.to_sym)};" }.join}".freeze
51
+ setup_wrapper(effective, connection_listener: connection_listener, error_handler: error_handler,
52
+ connection_event_inbox_capacity: connection_event_inbox_capacity,
53
+ error_event_inbox_capacity: error_event_inbox_capacity)
54
+ end
55
+
56
+ def self.from_conf(conf, connection_listener: nil, error_handler: nil,
57
+ connection_event_inbox_capacity: 64, error_event_inbox_capacity: 64, **overrides)
58
+ configuration, effective, protocol = merge_conf(conf, overrides)
59
+ object = allocate
60
+ object.send(:initialize_from_conf, configuration, effective, protocol,
61
+ connection_listener: connection_listener, error_handler: error_handler,
62
+ connection_event_inbox_capacity: connection_event_inbox_capacity,
63
+ error_event_inbox_capacity: error_event_inbox_capacity)
64
+ object
65
+ end
66
+
67
+ def self.from_env(**overrides)
68
+ conf = ENV["QDB_CLIENT_CONF"]
69
+ raise Ingress::IngressError.new("QDB_CLIENT_CONF is not set", code: :config_error) unless conf && !conf.empty?
70
+ from_conf(conf, **overrides)
71
+ end
72
+
73
+ def establish
74
+ ensure_thread!
75
+ raise Ingress::IngressError.new("standalone sender is closed", code: :invalid_api_call) if @closed
76
+ return self if @native
77
+ @native = Ingress::Native::StandaloneSender.new(
78
+ @configuration, !@connection_listener.nil?, @connection_event_inbox_capacity,
79
+ !@error_handler.nil?, @error_event_inbox_capacity
80
+ )
81
+ @buffer = Ingress::Buffer.send(:from_native, @native.new_buffer(@init_buf_size))
82
+ @batch_started = nil
83
+ start_dispatcher(:connection, @connection_listener, @connection_inbox) if @connection_listener
84
+ start_dispatcher(:error, @error_handler, @error_inbox) if @error_handler
85
+ self
86
+ end
87
+
88
+ def open
89
+ raise ArgumentError, "a block is required" unless block_given?
90
+ establish
91
+ begin
92
+ result = yield self
93
+ close
94
+ result
95
+ rescue Exception
96
+ close(flush: false, drain: false)
97
+ raise
98
+ end
99
+ end
100
+
101
+ def new_buffer
102
+ ensure_open!
103
+ Ingress::Buffer.send(:from_native, @native.new_buffer(@init_buf_size))
104
+ end
105
+
106
+ def row(table, symbols: EMPTY_HASH, columns: EMPTY_HASH, at:)
107
+ ensure_open!
108
+ appended = @buffer.__send__(:append, table, symbols, columns, at)
109
+ return self unless appended
110
+ @batch_started ||= monotonic
111
+ flush if should_auto_flush?
112
+ self
113
+ end
114
+
115
+ # One-off poolless direct columnar load using this sender's auth/TLS
116
+ # configuration. It uses a separate connection and has no ordering
117
+ # relationship with the buffered row stream.
118
+ def ingest_columns(table, columns:, symbols: EMPTY_HASH, at:, max_rows_per_batch: 16_384, ack: :ok)
119
+ prepared = DirectIngestion.prepare(
120
+ table, columns: columns, symbols: symbols, at: at,
121
+ max_rows_per_batch: max_rows_per_batch, ack: ack
122
+ )
123
+ ensure_ws!
124
+ return self if prepared[4].zero?
125
+ @native.ingest_columns(@configuration, *prepared)
126
+ self
127
+ end
128
+ alias write_columns ingest_columns
129
+
130
+ def row_count = (@buffer&.row_count || 0)
131
+ alias length row_count
132
+ def empty? = row_count.zero?
133
+ def closed? = @closed || (@native && @native.closed?)
134
+ def established? = !@native.nil? && !closed?
135
+ def auto_flush? = @auto_flush_enabled
136
+ alias auto_flush auto_flush?
137
+
138
+ def flush(buffer: nil, clear: true, wait: false, ack: :ok)
139
+ ensure_open!
140
+ target = buffer || @buffer
141
+ raise TypeError, "buffer must be a QuestDB QWP buffer" unless target.is_a?(Ingress::Buffer)
142
+ raise ArgumentError, "clear: false requires an explicit buffer" if target.equal?(@buffer) && !clear
143
+ level = normalize_ack(ack)
144
+ raise ArgumentError, "ack: :durable requires wait: true" if level == ACK_LEVELS[:durable] && !wait
145
+ return self if target.empty? && !wait
146
+ @native.flush(target.send(:native), !clear)
147
+ @native.wait(level, 0) if wait
148
+ @batch_started = nil if target.equal?(@buffer) && target.empty?
149
+ self
150
+ end
151
+
152
+ def wait(timeout_millis: 0, ack: :ok)
153
+ ensure_ws!
154
+ @native.wait(normalize_ack(ack), non_negative_integer(timeout_millis, "timeout_millis"))
155
+ self
156
+ end
157
+
158
+ def flush_and_get_fsn
159
+ ensure_ws!
160
+ value = @native.flush_fsn(@buffer.send(:native), false)
161
+ @batch_started = nil if @buffer.empty?
162
+ value
163
+ end
164
+
165
+ def flush_and_keep_and_get_fsn(buffer = nil)
166
+ ensure_ws!
167
+ target = buffer || @buffer
168
+ @native.flush_fsn(target.send(:native), true)
169
+ end
170
+
171
+ def published_fsn = (ensure_ws!; @native.published_fsn)
172
+ def acked_fsn = (ensure_ws!; @native.acked_fsn)
173
+
174
+ def await_acked_fsn(fsn, timeout_millis: 0)
175
+ ensure_ws!
176
+ target = non_negative_integer(fsn, "fsn")
177
+ current = @native.acked_fsn
178
+ return true if current && current >= target
179
+ begin
180
+ @native.wait(ACK_LEVELS[:ok], non_negative_integer(timeout_millis, "timeout_millis"))
181
+ rescue Ingress::IngressError => error
182
+ raise unless error.code == :failover_retry
183
+ end
184
+ current = @native.acked_fsn
185
+ !!(current && current >= target)
186
+ end
187
+
188
+ def drive_once = (ensure_ws!; @native.drive_once)
189
+
190
+ def close_drain
191
+ ensure_ws!
192
+ @native.close_drain
193
+ self
194
+ end
195
+
196
+ def poll_error
197
+ ensure_ws!
198
+ values = @native.poll_error
199
+ values && SenderError.from_native(values)
200
+ end
201
+
202
+ def poll_connection_event = @connection_inbox&.poll
203
+ def poll_callback_error = @error_inbox&.poll
204
+
205
+ def error_events_delivered
206
+ ensure_ws!
207
+ @native.error_events_delivered + (@error_inbox&.delivered || 0)
208
+ end
209
+
210
+ def error_events_dropped
211
+ ensure_ws!
212
+ @native.error_events_dropped + (@error_inbox&.dropped || 0)
213
+ end
214
+
215
+ def connection_events_delivered
216
+ ensure_open!
217
+ @native.connection_events_delivered + (@connection_inbox&.delivered || 0)
218
+ end
219
+
220
+ def connection_events_dropped
221
+ ensure_open!
222
+ @native.connection_events_dropped + (@connection_inbox&.dropped || 0)
223
+ end
224
+
225
+ def close(flush: true, wait: false, drain: nil)
226
+ return self if @closed
227
+ ensure_thread!
228
+ drain = flush if drain.nil?
229
+ begin
230
+ if @native && flush && @buffer && !@buffer.empty?
231
+ self.flush(wait: wait)
232
+ elsif @native && wait
233
+ self.wait
234
+ end
235
+ ensure
236
+ primary = $!
237
+ @buffer&.clear
238
+ begin
239
+ @native&.close(!!drain)
240
+ rescue Exception
241
+ raise unless primary
242
+ ensure
243
+ @closed = true
244
+ threads = @dispatcher_threads
245
+ @dispatcher_threads = []
246
+ current = Thread.current
247
+ threads.each { |thread| thread.join unless thread.equal?(current) }
248
+ end
249
+ end
250
+ self
251
+ end
252
+
253
+ def initialize_copy(*) = raise(TypeError, "QuestDB standalone senders cannot be copied")
254
+
255
+ class << self
256
+ private
257
+
258
+ def dispatcher_proc(owner, native, kind, handler, inbox)
259
+ method = kind == :connection ? :next_connection_event : :next_error_event
260
+ converter = kind == :connection ? ConnectionEvent : SenderError
261
+ proc do
262
+ Thread.current.name = "questdb-standalone-#{kind}-events" if Thread.current.respond_to?(:name=)
263
+ loop do
264
+ break unless owner.weakref_alive?
265
+ values = native.public_send(method, false)
266
+ unless values
267
+ break if native.closed?
268
+ sleep(0.01)
269
+ next
270
+ end
271
+ event = converter.from_native(values)
272
+ inbox.push(event)
273
+ begin
274
+ handler.call(event)
275
+ rescue Exception => error
276
+ Kernel.warn("QuestDB standalone #{kind} callback failed: #{error.class}: #{error.message}")
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ def merge_conf(conf, overrides)
283
+ conf = String(conf)
284
+ schema, separator, rest = conf.partition("::")
285
+ protocol = schema.to_sym
286
+ unless separator == "::" && PROTOCOLS.include?(protocol)
287
+ raise Ingress::IngressError.new("configuration must start with ws:: or wss::", code: :config_error)
288
+ end
289
+ overrides = overrides.transform_keys(&:to_sym)
290
+ overrides.each_key do |key|
291
+ raise ArgumentError, "invalid setting #{key.inspect}" unless key.to_s.match?(/\A[a-z_][a-z0-9_]*\z/)
292
+ end
293
+ parts = Config.split_parts(rest)
294
+ keys = parts.filter_map do |part|
295
+ key, value = part.split("=", 2)
296
+ key if value && key.match?(/\A[a-z_][a-z0-9_]*\z/)
297
+ end
298
+ duplicates = keys.tally.select { |_key, count| count > 1 }.keys
299
+ unless duplicates.empty?
300
+ raise Ingress::IngressError.new("duplicate configuration keys: #{duplicates.join(', ')}", code: :config_error)
301
+ end
302
+ values = Config.parse(conf)
303
+ conflicts = overrides.keys.select { |key| values.key?(key.to_s) }
304
+ raise ArgumentError, "settings already present in configuration: #{conflicts.join(', ')}" unless conflicts.empty?
305
+ effective = values.merge(overrides.to_h { |key, value| [key.to_s, Config.setting_value(value, key)] })
306
+ native_parts = parts.reject { |part| Config::STANDALONE_WRAPPER_KEYS.include?(part.split("=", 2).first) }
307
+ keyword_parts = overrides.reject { |key, _| Config::STANDALONE_WRAPPER_KEYS.include?(key.to_s) }
308
+ .map { |key, value| "#{key}=#{Config.encode(value, key)};" }.join
309
+ rebuilt = native_parts.map do |part|
310
+ key, value = part.split("=", 2)
311
+ value ? "#{key}=#{Config.encode(value)};" : "#{Config.encode(part)};"
312
+ end.join
313
+ ["#{schema}::#{keyword_parts}#{rebuilt}".freeze, effective, protocol]
314
+ end
315
+ end
316
+
317
+ private
318
+
319
+ def initialize_from_conf(configuration, effective, protocol, connection_listener:, error_handler:,
320
+ connection_event_inbox_capacity:, error_event_inbox_capacity:)
321
+ @configuration = configuration
322
+ @protocol = protocol
323
+ setup_wrapper(effective, connection_listener: connection_listener, error_handler: error_handler,
324
+ connection_event_inbox_capacity: connection_event_inbox_capacity,
325
+ error_event_inbox_capacity: error_event_inbox_capacity)
326
+ end
327
+
328
+ def setup_wrapper(settings, connection_listener:, error_handler:,
329
+ connection_event_inbox_capacity:, error_event_inbox_capacity:)
330
+ @owner_thread = Thread.current
331
+ validate_handler!(connection_listener, "connection_listener")
332
+ validate_handler!(error_handler, "error_handler")
333
+ @connection_event_inbox_capacity = validate_capacity!(connection_event_inbox_capacity, "connection_event_inbox_capacity")
334
+ @error_event_inbox_capacity = validate_capacity!(error_event_inbox_capacity, "error_event_inbox_capacity")
335
+ @connection_listener = connection_listener
336
+ @error_handler = error_handler
337
+ @connection_inbox = connection_listener && EventInbox.new(@connection_event_inbox_capacity)
338
+ @error_inbox = error_handler && EventInbox.new(@error_event_inbox_capacity)
339
+ @dispatcher_threads = []
340
+ @native = @buffer = nil
341
+ @closed = false
342
+ @init_buf_size = Integer(settings.fetch("init_buf_size", 65_536))
343
+ raise ArgumentError, "init_buf_size must be nonnegative" if @init_buf_size.negative?
344
+ raise RangeError, "init_buf_size exceeds 100 MiB" if @init_buf_size > Ingress::Buffer::MAX_PREALLOCATE_BYTES
345
+ @auto_flush_rows = threshold(settings.fetch("auto_flush_rows", 600), "auto_flush_rows")
346
+ @auto_flush_bytes = settings.key?("auto_flush_bytes") ? threshold(settings["auto_flush_bytes"], "auto_flush_bytes") : nil
347
+ @auto_flush_interval = threshold(settings.fetch("auto_flush_interval", 1_000), "auto_flush_interval")
348
+ enabled = if settings.key?("auto_flush")
349
+ switch(settings["auto_flush"], "auto_flush")
350
+ else
351
+ [@auto_flush_rows, @auto_flush_bytes, @auto_flush_interval].any?
352
+ end
353
+ unless enabled
354
+ @auto_flush_rows = @auto_flush_bytes = @auto_flush_interval = nil
355
+ end
356
+ if enabled && [@auto_flush_rows, @auto_flush_bytes, @auto_flush_interval].none?
357
+ raise ArgumentError, "auto_flush cannot be enabled when every threshold is off"
358
+ end
359
+ @auto_flush_enabled = enabled
360
+ @batch_started = nil
361
+ end
362
+
363
+ def start_dispatcher(kind, handler, inbox)
364
+ thread = Thread.new(&self.class.send(:dispatcher_proc, WeakRef.new(self), @native, kind, handler, inbox))
365
+ thread.report_on_exception = false if thread.respond_to?(:report_on_exception=)
366
+ @dispatcher_threads << thread
367
+ end
368
+
369
+ def validate_handler!(handler, name)
370
+ return if handler.nil? || handler.respond_to?(:call)
371
+ raise TypeError, "#{name} must respond to #call"
372
+ end
373
+
374
+ def validate_capacity!(value, name)
375
+ capacity = Integer(value)
376
+ raise ArgumentError, "#{name} must be nonnegative" if capacity.negative?
377
+ capacity
378
+ rescue ArgumentError, TypeError
379
+ raise ArgumentError, "#{name} must be a nonnegative integer"
380
+ end
381
+
382
+ def ensure_thread!
383
+ return if Thread.current.equal?(@owner_thread)
384
+ raise Ingress::IngressError.new("standalone sender must be used on its creating thread", code: :invalid_api_call)
385
+ end
386
+
387
+ def ensure_open!
388
+ ensure_thread!
389
+ raise Ingress::IngressError.new("standalone sender is not established", code: :invalid_api_call) unless established?
390
+ end
391
+
392
+ def ensure_ws!
393
+ ensure_open!
394
+ true
395
+ end
396
+
397
+ def normalize_ack(value)
398
+ value = value.to_sym if value.respond_to?(:to_sym)
399
+ ACK_LEVELS.fetch(value) { raise ArgumentError, "ack must be :ok or :durable" }
400
+ end
401
+
402
+ def threshold(value, name)
403
+ return nil if %w[off false].include?(value.to_s)
404
+ number = Integer(value)
405
+ raise ArgumentError, "#{name} must be positive or off" unless number.positive?
406
+ number
407
+ end
408
+
409
+ def switch(value, name)
410
+ return true if %w[on true].include?(value.to_s)
411
+ return false if %w[off false].include?(value.to_s)
412
+ raise ArgumentError, "#{name} must be on or off"
413
+ end
414
+
415
+ def validate_setting_keys!(settings)
416
+ settings.each_key do |key|
417
+ raise ArgumentError, "invalid setting #{key.inspect}" unless key.to_s.match?(/\A[a-z_][a-z0-9_]*\z/)
418
+ end
419
+ end
420
+
421
+ def non_negative_integer(value, name)
422
+ value = Integer(value)
423
+ raise ArgumentError, "#{name} must be nonnegative" if value.negative?
424
+ value
425
+ end
426
+
427
+ def should_auto_flush?
428
+ return false unless @batch_started
429
+ return true if @auto_flush_rows && row_count >= @auto_flush_rows
430
+ return true if @auto_flush_bytes && @buffer.bytesize >= @auto_flush_bytes
431
+ @auto_flush_interval && monotonic - @batch_started >= @auto_flush_interval
432
+ end
433
+
434
+ def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
435
+ end
436
+ end
@@ -0,0 +1,49 @@
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
+ class SenderError
27
+ ATTRIBUTES = %i[category applied_policy status message message_sequence from_fsn to_fsn].freeze
28
+ attr_reader(*ATTRIBUTES)
29
+
30
+ def initialize(category:, applied_policy:, status:, message:, message_sequence:, from_fsn:, to_fsn:)
31
+ @category = category.to_sym
32
+ @applied_policy = applied_policy.to_sym
33
+ @status = status
34
+ @message = String(message).dup.freeze
35
+ @message_sequence = message_sequence
36
+ @from_fsn = Integer(from_fsn)
37
+ @to_fsn = Integer(to_fsn)
38
+ freeze
39
+ end
40
+
41
+ def terminal? = applied_policy == :terminal
42
+ def retriable? = %i[retriable retriable_other].include?(applied_policy)
43
+
44
+ def self.from_native(values)
45
+ new(category: values[0], applied_policy: values[1], status: values[2],
46
+ message: values[3], message_sequence: values[4], from_fsn: values[5], to_fsn: values[6])
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,198 @@
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
+ require "ipaddr"
25
+
26
+ module QuestDB
27
+ class DateMillis
28
+ attr_reader :value
29
+ def initialize(value)
30
+ @value = Integer(value)
31
+ raise RangeError, "date milliseconds are outside signed 64-bit range" unless @value.between?(-(1 << 63), (1 << 63) - 1)
32
+ freeze
33
+ end
34
+ def to_time = Time.at(Rational(value, 1_000)).utc
35
+ def ==(other) = other.is_a?(DateMillis) && value == other.value
36
+ end
37
+
38
+ class Char
39
+ attr_reader :value
40
+ def initialize(value)
41
+ value = value.ord if value.is_a?(String) && value.length == 1
42
+ @value = Integer(value)
43
+ raise RangeError, "CHAR must be one UTF-16 code unit" unless @value.between?(0, 0xffff)
44
+ freeze
45
+ end
46
+ def to_s
47
+ return "\uFFFD" if value.between?(0xd800, 0xdfff)
48
+ [value].pack("U")
49
+ end
50
+ def ==(other) = other.is_a?(Char) && value == other.value
51
+ end
52
+
53
+ class UUID
54
+ PATTERN = /\A([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\z/
55
+ attr_reader :bytes
56
+ def initialize(value)
57
+ if value.is_a?(UUID)
58
+ @bytes = value.bytes
59
+ else
60
+ match = PATTERN.match(String(value)) or raise ArgumentError, "invalid UUID"
61
+ @bytes = match.captures.join.then { |hex| [hex].pack("H*") }.freeze
62
+ end
63
+ freeze
64
+ end
65
+ def self.from_bytes(value)
66
+ bytes = String(value).b
67
+ raise ArgumentError, "UUID requires exactly 16 bytes" unless bytes.bytesize == 16
68
+ allocate.tap { |uuid| uuid.instance_variable_set(:@bytes, bytes.dup.freeze); uuid.freeze }
69
+ end
70
+ def to_s
71
+ hex = bytes.unpack1("H*")
72
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
73
+ end
74
+ alias inspect to_s
75
+ def ==(other) = other.is_a?(UUID) && bytes == other.bytes
76
+ end
77
+
78
+ class Binary
79
+ attr_reader :bytes
80
+ def initialize(bytes)
81
+ @bytes = String(bytes).b.dup.freeze
82
+ freeze
83
+ end
84
+ def ==(other) = other.is_a?(Binary) && bytes == other.bytes
85
+ end
86
+
87
+ class IPv4
88
+ attr_reader :value
89
+ def initialize(value)
90
+ @value = case value
91
+ when IPv4 then value.value
92
+ when IPAddr
93
+ raise ArgumentError, "IPv4 value must not be IPv6" unless value.ipv4?
94
+ value.to_i
95
+ when String
96
+ address = IPAddr.new(value)
97
+ raise ArgumentError, "IPv4 value must not be IPv6" unless address.ipv4?
98
+ address.to_i
99
+ else Integer(value)
100
+ end
101
+ raise RangeError, "IPv4 value must fit unsigned 32-bit" unless @value.between?(0, 0xffff_ffff)
102
+ freeze
103
+ end
104
+ def to_s = [24, 16, 8, 0].map { |shift| (value >> shift) & 0xff }.join(".")
105
+ def to_ipaddr = IPAddr.new(value, Socket::AF_INET)
106
+ def ==(other) = other.is_a?(IPv4) && value == other.value
107
+ end
108
+
109
+ class Geohash
110
+ attr_reader :value, :precision_bits
111
+ def self.from_native(value, precision_bits) = new(value, precision_bits: precision_bits)
112
+ def initialize(value, precision_bits:)
113
+ @value = Integer(value)
114
+ @precision_bits = Integer(precision_bits)
115
+ raise RangeError, "geohash precision must be between 1 and 60 bits" unless @precision_bits.between?(1, 60)
116
+ raise RangeError, "geohash value exceeds its precision" unless @value.between?(0, (1 << @precision_bits) - 1)
117
+ freeze
118
+ end
119
+ def ==(other) = other.is_a?(Geohash) && value == other.value && precision_bits == other.precision_bits
120
+ end
121
+
122
+ class Long256
123
+ LIMIT = 1 << 256
124
+ attr_reader :value
125
+ def initialize(value)
126
+ @value = Integer(value)
127
+ raise RangeError, "LONG256 must be between 0 and 2^256-1" unless @value.between?(0, LIMIT - 1)
128
+ freeze
129
+ end
130
+ def bytes = [value.to_s(16).rjust(64, "0")].pack("H*").reverse
131
+ def self.from_bytes(bytes)
132
+ bytes = String(bytes).b
133
+ raise ArgumentError, "LONG256 requires 32 bytes" unless bytes.bytesize == 32
134
+ new(bytes.reverse.unpack1("H*").to_i(16))
135
+ end
136
+ def ==(other) = other.is_a?(Long256) && value == other.value
137
+ end
138
+
139
+ class DecimalValue
140
+ attr_reader :mantissa, :scale
141
+ def self.from_native(mantissa, scale) = new(mantissa, scale: scale)
142
+ def initialize(mantissa, scale:)
143
+ @mantissa = Integer(mantissa)
144
+ @scale = Integer(scale)
145
+ raise RangeError, "decimal scale must fit signed 8-bit" unless @scale.between?(-128, 127)
146
+ freeze
147
+ end
148
+ def self.from_bigdecimal(value)
149
+ sign, digits, _base, exponent = value.split
150
+ mantissa = (sign < 0 ? -1 : 1) * digits.to_i
151
+ scale = digits.length - exponent
152
+ while scale.positive? && (mantissa % 10).zero?
153
+ mantissa /= 10
154
+ scale -= 1
155
+ end
156
+ new(mantissa, scale: scale)
157
+ end
158
+ def self.from_twos_complement(bytes, scale)
159
+ bytes = String(bytes).b
160
+ bits = bytes.bytesize * 8
161
+ unsigned = bytes.reverse.unpack1("H*").to_i(16)
162
+ unsigned -= 1 << bits if (unsigned & (1 << (bits - 1))) != 0
163
+ new(unsigned, scale: scale)
164
+ end
165
+ def to_d = BigDecimal(mantissa) * (BigDecimal(10) ** -scale)
166
+ def to_s = to_d.to_s("F")
167
+ def ==(other) = other.is_a?(DecimalValue) && mantissa == other.mantissa && scale == other.scale
168
+ end
169
+
170
+ class TypedNull
171
+ KINDS = %i[boolean byte short int long float double symbol timestamp date uuid long256 geohash varchar timestamp_nanos decimal64 decimal128 decimal256 char binary ipv4].freeze
172
+ attr_reader :kind, :scale, :precision_bits
173
+ def initialize(kind, scale: nil, precision_bits: nil)
174
+ @kind = kind.to_sym
175
+ raise ArgumentError, "unsupported typed NULL kind #{@kind.inspect}" unless KINDS.include?(@kind)
176
+ @scale = scale.nil? ? nil : Integer(scale)
177
+ @precision_bits = precision_bits.nil? ? nil : Integer(precision_bits)
178
+ if %i[decimal64 decimal128 decimal256].include?(@kind)
179
+ @scale ||= 0
180
+ raise RangeError, "decimal scale must fit signed 8-bit" unless @scale.between?(-128, 127)
181
+ end
182
+ if @kind == :geohash
183
+ raise ArgumentError, "geohash NULL requires precision_bits:" unless @precision_bits
184
+ raise RangeError, "geohash precision must be between 1 and 60 bits" unless @precision_bits.between?(1, 60)
185
+ end
186
+ freeze
187
+ end
188
+ end
189
+
190
+ class OpaqueValue
191
+ attr_reader :kind, :bytes
192
+ def initialize(kind, bytes = nil)
193
+ @kind = kind
194
+ @bytes = bytes&.b&.dup&.freeze
195
+ freeze
196
+ end
197
+ end
198
+ end