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,60 @@
1
+ # ___ _ ____ ____
2
+ # / _ \ _ _ ___ ___| |_| _ \| __ )
3
+ # | | | | | | |/ _ \/ __| __| | | | _ \
4
+ # | |_| | |_| | __/\__ \ |_| |_| | |_) |
5
+ # \__\_\\__,_|\___||___/\__|____/|____/
6
+ #
7
+ # Copyright (c) 2014-2019 Appsicle
8
+ # Copyright (c) 2019-2026 QuestDB
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the "License");
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at
13
+ #
14
+ # http://www.apache.org/licenses/LICENSE-2.0
15
+ #
16
+ # Unless required by applicable law or agreed to in writing, software
17
+ # distributed under the License is distributed on an "AS IS" BASIS,
18
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ # See the License for the specific language governing permissions and
20
+ # limitations under the License.
21
+ #
22
+ module QuestDB
23
+ module Ingress
24
+ class Float64Array
25
+ attr_reader :values, :shape
26
+ def initialize(values, shape:)
27
+ raise TypeError, "values and shape must be Arrays" unless values.is_a?(Array) && shape.is_a?(Array)
28
+ @shape = shape.map { |d| Integer(d) }
29
+ raise ArgumentError, "dimensions must be non-negative" unless @shape.all? { |dimension| dimension >= 0 }
30
+ expected = if @shape.empty?
31
+ 0
32
+ else
33
+ @shape.reduce(1) do |n, d|
34
+ next 0 if d.zero? || n.zero?
35
+ raise RangeError, "shape is too large" if n > ((1 << 63) - 1) / d
36
+ n * d
37
+ end
38
+ end
39
+ raise ArgumentError, "shape requires #{expected} values, got #{values.length}" unless expected == values.length
40
+ @values = values.map { |v| Float(v) }.freeze
41
+ @shape.freeze
42
+ freeze
43
+ end
44
+ def self.from_native(values, shape) = new(values, shape: shape)
45
+ def self.from_nested(value)
46
+ raise TypeError, "nested value must be an Array" unless value.is_a?(Array)
47
+ walk = lambda do |node|
48
+ return [[], [Float(node)]] unless node.is_a?(Array)
49
+ return [[0], []] if node.empty?
50
+ children = node.map { |child| walk.call(child) }
51
+ child_shape = children.first.first
52
+ raise ArgumentError, "ragged nested array" unless children.all? { |shape, _| shape == child_shape }
53
+ [[node.length, *child_shape], children.flat_map(&:last)]
54
+ end
55
+ shape, values = walk.call(value)
56
+ new(values, shape: shape)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ # ___ _ ____ ____
2
+ # / _ \ _ _ ___ ___| |_| _ \| __ )
3
+ # | | | | | | |/ _ \/ __| __| | | | _ \
4
+ # | |_| | |_| | __/\__ \ |_| |_| | |_) |
5
+ # \__\_\\__,_|\___||___/\__|____/|____/
6
+ #
7
+ # Copyright (c) 2014-2019 Appsicle
8
+ # Copyright (c) 2019-2026 QuestDB
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the "License");
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at
13
+ #
14
+ # http://www.apache.org/licenses/LICENSE-2.0
15
+ #
16
+ # Unless required by applicable law or agreed to in writing, software
17
+ # distributed under the License is distributed on an "AS IS" BASIS,
18
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ # See the License for the specific language governing permissions and
20
+ # limitations under the License.
21
+ #
22
+ # Source installs place the extension on RubyGems' extension load path.
23
+ # Precompiled gems carry one binary per supported Ruby minor because MRI's C
24
+ # ABI and libruby soname are not stable across minor releases.
25
+ begin
26
+ require "questdb_ingress"
27
+ rescue LoadError => source_error
28
+ require "rbconfig"
29
+ ruby_abi = RUBY_VERSION[/\A\d+\.\d+/]
30
+ native = File.expand_path("../native/#{ruby_abi}/questdb_ingress", __dir__)
31
+ if File.file?("#{native}.#{RbConfig::CONFIG.fetch('DLEXT')}")
32
+ require native
33
+ else
34
+ begin
35
+ require_relative "../../../ext/questdb_ingress/questdb_ingress"
36
+ rescue LoadError
37
+ raise source_error
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ # ___ _ ____ ____
2
+ # / _ \ _ _ ___ ___| |_| _ \| __ )
3
+ # | | | | | | |/ _ \/ __| __| | | | _ \
4
+ # | |_| | |_| | __/\__ \ |_| |_| | |_) |
5
+ # \__\_\\__,_|\___||___/\__|____/|____/
6
+ #
7
+ # Copyright (c) 2014-2019 Appsicle
8
+ # Copyright (c) 2019-2026 QuestDB
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the "License");
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at
13
+ #
14
+ # http://www.apache.org/licenses/LICENSE-2.0
15
+ #
16
+ # Unless required by applicable law or agreed to in writing, software
17
+ # distributed under the License is distributed on an "AS IS" BASIS,
18
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ # See the License for the specific language governing permissions and
20
+ # limitations under the License.
21
+ #
22
+ module QuestDB
23
+ module Ingress
24
+ class Timestamp
25
+ MIN = -(1 << 63)
26
+ MAX = (1 << 63) - 1
27
+ attr_reader :value
28
+ def initialize(value)
29
+ raise TypeError, "timestamp must be an Integer" unless value.is_a?(Integer)
30
+ raise RangeError, "timestamp must fit signed 64-bit" unless value.between?(MIN, MAX)
31
+ @value = value
32
+ freeze
33
+ end
34
+ def ==(other) = other.class == self.class && other.value == value
35
+ end
36
+ class TimestampNanos < Timestamp
37
+ def self.now = new(Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond))
38
+ def self.from_time(time)
39
+ raise TypeError, "expected Time" unless time.is_a?(Time)
40
+ new(time.to_i * 1_000_000_000 + time.nsec)
41
+ end
42
+ end
43
+ class TimestampMicros < Timestamp
44
+ def self.now = new(Process.clock_gettime(Process::CLOCK_REALTIME, :microsecond))
45
+ def self.from_time(time)
46
+ raise TypeError, "expected Time" unless time.is_a?(Time)
47
+ new(time.to_i * 1_000_000 + time.usec)
48
+ end
49
+ end
50
+ SERVER_TIMESTAMP = Object.new.freeze
51
+ ServerTimestamp = SERVER_TIMESTAMP
52
+ end
53
+ end
@@ -0,0 +1,77 @@
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 "bigdecimal"
25
+ require_relative "version"
26
+ require_relative "ingress/error"
27
+ require_relative "ingress/timestamp"
28
+ require_relative "ingress/float64_array"
29
+ require_relative "types"
30
+ require_relative "column_vector"
31
+ require_relative "direct_ingestion"
32
+ require_relative "query"
33
+ require_relative "ingress/native"
34
+ require_relative "config"
35
+ require_relative "sender_error"
36
+ require_relative "connection_event"
37
+ require_relative "event_inbox"
38
+ require_relative "ingress/buffer"
39
+ require_relative "pooled_sender"
40
+ require_relative "sender"
41
+ require_relative "client"
42
+
43
+ module QuestDB
44
+ VERSION = Ingress::VERSION unless const_defined?(:VERSION, false)
45
+ C_CLIENT_VERSION = Ingress::C_CLIENT_VERSION unless const_defined?(:C_CLIENT_VERSION, false)
46
+ Error = Ingress::IngressError unless const_defined?(:Error, false)
47
+ QuestDBError = Ingress::IngressError unless const_defined?(:QuestDBError, false)
48
+ ServerRejectionError = Ingress::ServerRejectionError unless const_defined?(:ServerRejectionError, false)
49
+ QuestDBServerRejectionError = Ingress::ServerRejectionError unless const_defined?(:QuestDBServerRejectionError, false)
50
+ Timestamp = Ingress::Timestamp unless const_defined?(:Timestamp, false)
51
+ TimestampNanos = Ingress::TimestampNanos unless const_defined?(:TimestampNanos, false)
52
+ TimestampMicros = Ingress::TimestampMicros unless const_defined?(:TimestampMicros, false)
53
+ Float64Array = Ingress::Float64Array unless const_defined?(:Float64Array, false)
54
+ SERVER_TIMESTAMP = Ingress::SERVER_TIMESTAMP unless const_defined?(:SERVER_TIMESTAMP, false)
55
+
56
+ module_function
57
+
58
+ def connect(conf = nil, host: nil, port: nil, tls: nil, connection_listener: nil,
59
+ error_handler: nil,
60
+ connection_event_inbox_capacity: Client::DEFAULT_EVENT_INBOX_CAPACITY,
61
+ error_event_inbox_capacity: Client::DEFAULT_EVENT_INBOX_CAPACITY, **settings)
62
+ configuration, effective = Config.build(conf, host: host, port: port, tls: tls, settings: settings)
63
+ client = Client.new(
64
+ configuration, effective_settings: effective,
65
+ connection_listener: connection_listener, error_handler: error_handler,
66
+ connection_event_inbox_capacity: connection_event_inbox_capacity,
67
+ error_event_inbox_capacity: error_event_inbox_capacity
68
+ )
69
+ return client unless block_given?
70
+
71
+ begin
72
+ yield client
73
+ ensure
74
+ client.close
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,215 @@
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 PooledSender
27
+ ACK_LEVELS = { ok: 0, durable: 1 }.freeze
28
+ EMPTY_HASH = {}.freeze
29
+ private_constant :EMPTY_HASH
30
+
31
+ attr_reader :auto_flush_rows, :auto_flush_bytes, :auto_flush_interval
32
+
33
+ def initialize(client, native, auto_flush:)
34
+ @client = client
35
+ @native = native
36
+ @owner_thread = Thread.current
37
+ @closed = false
38
+ @buffer = Ingress::Buffer.send(:from_native, @native.new_buffer)
39
+ @auto_flush_rows = auto_flush.fetch(:rows)
40
+ @auto_flush_bytes = auto_flush.fetch(:bytes)
41
+ @auto_flush_interval = auto_flush.fetch(:interval)
42
+ @dynamic_auto_flush_bytes = auto_flush.fetch(:dynamic_bytes)
43
+ @batch_started = nil
44
+ end
45
+
46
+ def row(table, symbols: EMPTY_HASH, columns: EMPTY_HASH, at:)
47
+ ensure_open!
48
+ appended = @buffer.__send__(:append, table, symbols, columns, at)
49
+ return self unless appended
50
+ @batch_started ||= monotonic
51
+ flush if should_auto_flush?
52
+ self
53
+ end
54
+
55
+ def row_count = (@buffer&.row_count || 0)
56
+ alias length row_count
57
+ def empty? = row_count.zero?
58
+ def closed? = @closed || @native.closed?
59
+
60
+ def flush(wait: false, ack: :ok)
61
+ ensure_open!
62
+ level = normalize_ack(ack)
63
+ raise ArgumentError, "ack: :durable requires wait: true" if level == ACK_LEVELS[:durable] && !wait
64
+ return self if empty? && !wait
65
+ @native.flush(@buffer.send(:native), !!wait, level)
66
+ @batch_started = nil if @buffer.empty?
67
+ self
68
+ end
69
+
70
+ def wait(timeout_millis: 0, ack: :ok)
71
+ ensure_open!
72
+ timeout = non_negative_integer(timeout_millis, "timeout_millis")
73
+ @native.wait(normalize_ack(ack), timeout)
74
+ self
75
+ end
76
+
77
+ def flush_and_get_fsn
78
+ ensure_open!
79
+ fsn = @native.flush_fsn(@buffer.send(:native), false)
80
+ @batch_started = nil if @buffer.empty?
81
+ fsn
82
+ end
83
+
84
+ def flush_and_keep_and_get_fsn
85
+ ensure_open!
86
+ @native.flush_fsn(@buffer.send(:native), true)
87
+ end
88
+
89
+ def published_fsn
90
+ ensure_open!
91
+ @native.published_fsn
92
+ end
93
+
94
+ def acked_fsn
95
+ ensure_open!
96
+ @native.acked_fsn
97
+ end
98
+
99
+ def await_acked_fsn(fsn, timeout_millis: 0)
100
+ ensure_open!
101
+ fsn = non_negative_integer(fsn, "fsn")
102
+ timeout = non_negative_integer(timeout_millis, "timeout_millis")
103
+ current = @native.acked_fsn
104
+ return true if current && current >= fsn
105
+ begin
106
+ @native.wait(ACK_LEVELS[:ok], timeout)
107
+ rescue Ingress::IngressError => error
108
+ raise unless error.code == :failover_retry
109
+ end
110
+ current = @native.acked_fsn
111
+ !!(current && current >= fsn)
112
+ end
113
+
114
+ # Convenience parity with Client#ingest_columns. This uses a separate
115
+ # direct connection and therefore has no ordering relationship with this
116
+ # lease's buffered row stream.
117
+ def ingest_columns(table, columns:, symbols: EMPTY_HASH, at:, max_rows_per_batch: 16_384, ack: :ok)
118
+ ensure_open!
119
+ @client.ingest_columns(table, columns: columns, symbols: symbols, at: at,
120
+ max_rows_per_batch: max_rows_per_batch, ack: ack)
121
+ self
122
+ end
123
+ alias write_columns ingest_columns
124
+
125
+ def poll_error
126
+ ensure_open!
127
+ values = @native.poll_error
128
+ values && SenderError.from_native(values)
129
+ end
130
+
131
+ def error_events_dropped
132
+ ensure_open!
133
+ @native.error_events_dropped
134
+ end
135
+
136
+ def close(flush: true, wait: false)
137
+ return self if @closed
138
+ ensure_thread!
139
+ begin
140
+ if flush && !empty?
141
+ self.flush(wait: wait)
142
+ elsif wait
143
+ self.wait
144
+ end
145
+ ensure
146
+ @buffer&.clear
147
+ @native.close(false)
148
+ @closed = true
149
+ @client.send(:sender_released, self)
150
+ end
151
+ self
152
+ end
153
+
154
+ def open
155
+ raise ArgumentError, "a block is required" unless block_given?
156
+ begin
157
+ result = yield self
158
+ close
159
+ result
160
+ rescue Exception
161
+ close(flush: false)
162
+ raise
163
+ end
164
+ end
165
+
166
+ def initialize_copy(*) = raise(TypeError, "QuestDB pooled senders cannot be copied")
167
+
168
+ private
169
+
170
+ def ensure_thread!
171
+ return if Thread.current.equal?(@owner_thread)
172
+ raise Ingress::IngressError.new("pooled sender must be used on the thread that borrowed it", code: :invalid_api_call)
173
+ end
174
+
175
+ def ensure_open!
176
+ ensure_thread!
177
+ raise Ingress::IngressError.new("pooled sender is closed", code: :invalid_api_call) if closed?
178
+ end
179
+
180
+ def normalize_ack(value)
181
+ value = value.to_sym if value.respond_to?(:to_sym)
182
+ ACK_LEVELS.fetch(value) do
183
+ raise ArgumentError, "ack must be :ok or :durable"
184
+ end
185
+ end
186
+
187
+ def non_negative_integer(value, name)
188
+ value = Integer(value)
189
+ raise ArgumentError, "#{name} must be non-negative" if value.negative?
190
+ value
191
+ end
192
+
193
+ def should_auto_flush?
194
+ return false unless @batch_started
195
+ return true if @auto_flush_rows && row_count >= @auto_flush_rows
196
+ if @dynamic_auto_flush_bytes || @auto_flush_bytes
197
+ encoded_cap = @native.effective_frame_cap
198
+ server_cap_known = encoded_cap.positive?
199
+ hard_cap = encoded_cap.abs
200
+ soft_cap = (hard_cap / 10) * 9 + ((hard_cap % 10) * 9) / 10
201
+ byte_limit = if @dynamic_auto_flush_bytes
202
+ server_cap_known ? soft_cap : [soft_cap, 8 * 1024 * 1024].min
203
+ else
204
+ [@auto_flush_bytes, soft_cap].min
205
+ end
206
+ return true if @buffer.bytesize >= byte_limit
207
+ end
208
+ @auto_flush_interval && monotonic - @batch_started >= @auto_flush_interval
209
+ end
210
+
211
+ def monotonic
212
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
213
+ end
214
+ end
215
+ end