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,46 @@
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
+ require "questdb"
26
+
27
+ client = nil
28
+ client = QuestDB.connect(
29
+ ENV.fetch("QDB_CLIENT_CONF", "ws::addr=localhost:9000;"),
30
+ connection_listener: lambda { |event|
31
+ warn "connection=#{event.kind} endpoint=#{event.host}:#{event.port}"
32
+ },
33
+ error_handler: lambda { |error|
34
+ warn "rejection=#{error.category} policy=#{error.applied_policy} fsn=#{error.from_fsn}-#{error.to_fsn}: #{error.message}"
35
+ }
36
+ )
37
+
38
+ begin
39
+ client.sender do |sender|
40
+ sender.row("ruby_callback_example", columns: { value: 1 }, at: QuestDB::SERVER_TIMESTAMP)
41
+ sender.flush(wait: true)
42
+ end
43
+ ensure
44
+ # A handler closure that captures client keeps it alive; always close explicitly.
45
+ client.close
46
+ end
@@ -0,0 +1,41 @@
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 "questdb"
25
+
26
+ conf = ENV.fetch("QDB_CLIENT_CONF", "ws::addr=localhost:9000;")
27
+ QuestDB.connect(conf) do |client|
28
+ now = QuestDB::TimestampNanos.now.value
29
+ client.ingest_columns(
30
+ "ruby_columnar_example",
31
+ columns: {
32
+ price: [2615.54, 2616.10, nil],
33
+ quantity: QuestDB::ColumnVector.int([3, 5, 8]),
34
+ venue_code: QuestDB::ColumnVector.varchar(["LDN", "NYC", "SGP"])
35
+ },
36
+ symbols: { symbol: ["ETH-USD", "BTC-USD", "ETH-USD"] },
37
+ at: 3.times.map { |index| QuestDB::TimestampNanos.new(now + index) },
38
+ max_rows_per_batch: 16_384,
39
+ ack: :ok
40
+ )
41
+ end
@@ -0,0 +1,39 @@
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
+ require "questdb"
23
+
24
+ QuestDB.connect("ws::addr=localhost:9000;sender_pool_max=4;") do |client|
25
+ workers = 2.times.map do |index|
26
+ Thread.new do
27
+ client.sender do |sender|
28
+ 100.times do |row|
29
+ sender.row("parallel_metrics",
30
+ symbols: { worker: index.to_s },
31
+ columns: { value: row },
32
+ at: QuestDB::TimestampNanos.now)
33
+ end
34
+ sender.flush(wait: true)
35
+ end
36
+ end
37
+ end
38
+ workers.each(&:join)
39
+ end
data/examples/query.rb ADDED
@@ -0,0 +1,28 @@
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
+ require "questdb"
23
+
24
+ QuestDB.connect(ENV.fetch("QDB_CLIENT_CONF", "ws::addr=localhost:9000;")) do |client|
25
+ client.query("SELECT table_name FROM tables() LIMIT $1", binds: [10]) do |result|
26
+ result.each { |row| puts row["table_name"] }
27
+ end
28
+ end
@@ -0,0 +1,33 @@
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
+ require "questdb"
23
+
24
+ conf = ENV.fetch("QDB_CLIENT_CONF", "ws::addr=localhost:9000;auto_flush=off;")
25
+ QuestDB::Sender.from_conf(conf).open do |sender|
26
+ sender.row(
27
+ "ruby_standalone_example",
28
+ symbols: { source: "ruby" },
29
+ columns: { value: 42 },
30
+ at: QuestDB::TimestampNanos.now
31
+ )
32
+ sender.flush(wait: true)
33
+ end
@@ -0,0 +1,33 @@
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
+ require "questdb"
23
+
24
+ conf = "wss::addr=#{ENV.fetch('QDB_HOST')}:#{ENV.fetch('QDB_PORT', '9000')};" \
25
+ "token=#{ENV.fetch('QDB_TOKEN')};" \
26
+ "tls_roots=#{ENV.fetch('QDB_TLS_ROOTS')};"
27
+
28
+ QuestDB.connect(conf) do |client|
29
+ client.sender do |sender|
30
+ sender.row("secure_metrics", columns: { value: 1 }, at: QuestDB::TimestampNanos.now)
31
+ sender.flush(wait: true)
32
+ end
33
+ end
@@ -0,0 +1,250 @@
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
+ require "questdb"
26
+
27
+ module QuestDB
28
+ # Optional Apache Arrow C Stream interoperability. Requiring this file adds
29
+ # adapters without loading red-arrow, nanoarrow, or polars-df.
30
+ module Arrow
31
+ ACK_LEVELS = { ok: 0, durable: 1 }.freeze
32
+ OVERRIDE_KINDS = {
33
+ symbol: 0,
34
+ ipv4: 1,
35
+ char: 2,
36
+ geohash: 3,
37
+ varchar: 4
38
+ }.freeze
39
+ private_constant :ACK_LEVELS, :OVERRIDE_KINDS
40
+
41
+ class PointerCapsule
42
+ attr_reader :name
43
+
44
+ def initialize(pointer, name: "arrow_array_stream")
45
+ @pointer = Integer(pointer)
46
+ @name = name.freeze
47
+ end
48
+
49
+ def to_i = @pointer
50
+ end
51
+ private_constant :PointerCapsule
52
+
53
+ module_function
54
+
55
+ def with_stream(source)
56
+ if source.respond_to?(:arrow_c_stream)
57
+ capsule = source.arrow_c_stream
58
+ validate_capsule!(capsule)
59
+ return yield capsule
60
+ end
61
+
62
+ reader = red_arrow_reader(source)
63
+ unless reader
64
+ raise TypeError,
65
+ "Arrow source must respond to #arrow_c_stream or be an Arrow::Table, Arrow::RecordBatch, or Arrow::RecordBatchReader"
66
+ end
67
+
68
+ pointer = reader.export
69
+ capsule = PointerCapsule.new(pointer)
70
+ begin
71
+ yield capsule
72
+ ensure
73
+ # Red Arrow allocates only the ArrowArrayStream struct shell here.
74
+ # Native ingestion consumes/releases its contents; the shell remains
75
+ # the exporter's responsibility.
76
+ GLib.free(pointer)
77
+ end
78
+ end
79
+
80
+ def validate_capsule!(capsule)
81
+ raise TypeError, "arrow_c_stream must return an object responding to #to_i" unless capsule.respond_to?(:to_i)
82
+ if capsule.respond_to?(:name) && capsule.name != "arrow_array_stream"
83
+ raise TypeError, "arrow_c_stream returned #{capsule.name.inspect}, expected \"arrow_array_stream\""
84
+ end
85
+ capsule
86
+ end
87
+
88
+ def red_arrow_reader(source)
89
+ return nil unless defined?(::Arrow::RecordBatchReader)
90
+ return source if source.is_a?(::Arrow::RecordBatchReader)
91
+ if defined?(::Arrow::Table) && source.is_a?(::Arrow::Table)
92
+ return ::Arrow::TableBatchReader.new(source)
93
+ end
94
+ if defined?(::Arrow::RecordBatch) && source.is_a?(::Arrow::RecordBatch)
95
+ return ::Arrow::RecordBatchReader.new([source])
96
+ end
97
+ nil
98
+ end
99
+
100
+ def normalize_ack(value)
101
+ value = value.to_sym if value.respond_to?(:to_sym)
102
+ ACK_LEVELS.fetch(value) { raise ArgumentError, "ack must be :ok or :durable" }
103
+ end
104
+
105
+ def normalize_at(value)
106
+ case value
107
+ when String, Symbol
108
+ [2, value]
109
+ when TimestampNanos
110
+ [1, non_negative_i64(value.value, "designated timestamp")]
111
+ when TimestampMicros
112
+ [1, non_negative_i64(value.value * 1_000, "designated timestamp")]
113
+ when Time
114
+ [1, non_negative_i64(value.to_i * 1_000_000_000 + value.nsec, "designated timestamp")]
115
+ else
116
+ return [0, nil] if value.equal?(SERVER_TIMESTAMP)
117
+ raise TypeError, "at must be a timestamp column name, TimestampNanos, TimestampMicros, Time, or SERVER_TIMESTAMP"
118
+ end
119
+ end
120
+
121
+ def normalize_overrides(symbols, overrides)
122
+ raise TypeError, "schema_overrides must be a Hash" unless overrides.is_a?(Hash)
123
+ descriptors = {}
124
+ case symbols
125
+ when :auto, nil
126
+ when Array
127
+ symbols.each { |name| descriptors[name.to_s] = [name, OVERRIDE_KINDS[:symbol], 0] }
128
+ else
129
+ raise ArgumentError, "symbols must be :auto or an Array of column names"
130
+ end
131
+
132
+ overrides.each do |name, specification|
133
+ kind, argument = if specification.is_a?(Array)
134
+ raise ArgumentError, "override arrays must contain [kind, argument]" unless specification.length == 2
135
+ specification
136
+ else
137
+ [specification, 0]
138
+ end
139
+ kind = kind.to_sym if kind.respond_to?(:to_sym)
140
+ code = OVERRIDE_KINDS.fetch(kind) do
141
+ raise ArgumentError, "unsupported Arrow schema override #{kind.inspect}"
142
+ end
143
+ argument = Integer(argument)
144
+ if kind == :geohash
145
+ raise RangeError, "geohash precision must be between 1 and 60 bits" unless argument.between?(1, 60)
146
+ elsif argument != 0
147
+ raise ArgumentError, "only a geohash override accepts an argument"
148
+ end
149
+ key = name.to_s
150
+ raise ArgumentError, "duplicate Arrow override for #{key.inspect}" if descriptors.key?(key)
151
+ descriptors[key] = [name, code, argument]
152
+ end
153
+ descriptors.values.freeze
154
+ end
155
+
156
+ def non_negative_i64(value, label)
157
+ value = Integer(value)
158
+ raise RangeError, "#{label} cannot be before the Unix epoch" if value.negative?
159
+ raise RangeError, "#{label} is outside signed 64-bit range" if value > (1 << 63) - 1
160
+ value
161
+ end
162
+ end
163
+
164
+ class Client
165
+ def ingest_arrow(table, source, at:, symbols: :auto, schema_overrides: {}, ack: :ok,
166
+ checkpoint_batches: 100)
167
+ at_kind, at_value = Arrow.normalize_at(at)
168
+ overrides = Arrow.normalize_overrides(symbols, schema_overrides)
169
+ ack_level = Arrow.normalize_ack(ack)
170
+ checkpoint_batches = Integer(checkpoint_batches)
171
+ raise ArgumentError, "checkpoint_batches must be positive" unless checkpoint_batches.positive?
172
+ @mutex.synchronize { ensure_open! }
173
+ Arrow.with_stream(source) do |capsule|
174
+ @native.ingest_arrow(
175
+ capsule, table, at_kind, at_value, overrides, ack_level, checkpoint_batches
176
+ )
177
+ end
178
+ self
179
+ end
180
+ end
181
+
182
+ class PooledSender
183
+ def ingest_arrow(table, source, at:, symbols: :auto, schema_overrides: {}, ack: :ok,
184
+ checkpoint_batches: 100)
185
+ ensure_open!
186
+ @client.ingest_arrow(
187
+ table, source, at: at, symbols: symbols,
188
+ schema_overrides: schema_overrides, ack: ack,
189
+ checkpoint_batches: checkpoint_batches
190
+ )
191
+ self
192
+ end
193
+ end
194
+
195
+ class Sender
196
+ def ingest_arrow(table, source, at:, symbols: :auto, schema_overrides: {}, ack: :ok,
197
+ checkpoint_batches: 100)
198
+ at_kind, at_value = Arrow.normalize_at(at)
199
+ overrides = Arrow.normalize_overrides(symbols, schema_overrides)
200
+ ack_level = Arrow.normalize_ack(ack)
201
+ checkpoint_batches = Integer(checkpoint_batches)
202
+ raise ArgumentError, "checkpoint_batches must be positive" unless checkpoint_batches.positive?
203
+ ensure_ws!
204
+ Arrow.with_stream(source) do |capsule|
205
+ @native.ingest_arrow(
206
+ @configuration, capsule, table, at_kind, at_value,
207
+ overrides, ack_level, checkpoint_batches
208
+ )
209
+ end
210
+ self
211
+ end
212
+ end
213
+
214
+ class QueryResult
215
+ # Return a one-shot Arrow C Stream capsule. The native query is fully
216
+ # materialized into Arrow-owned batches before this method returns, then
217
+ # the reader lease is closed. Consumers may retain the capsule independently.
218
+ def arrow_c_stream
219
+ start_consumption!
220
+ capsule = nil
221
+ begin
222
+ capsule = @native.materialize_arrow_stream
223
+ ensure
224
+ close_preserving_primary_error
225
+ end
226
+ capsule
227
+ end
228
+
229
+ def to_nanoarrow
230
+ require "nanoarrow"
231
+ Nanoarrow::Array.new(self)
232
+ end
233
+
234
+ def to_polars
235
+ require "polars-df"
236
+ Polars::DataFrame.new(self)
237
+ end
238
+
239
+ def to_red_arrow_table
240
+ require "arrow"
241
+ capsule = arrow_c_stream
242
+ reader = ::Arrow::RecordBatchReader.import(capsule.to_i)
243
+ begin
244
+ reader.read_all
245
+ ensure
246
+ reader.unref
247
+ end
248
+ end
249
+ end
250
+ end