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
data/docs/arrow.md ADDED
@@ -0,0 +1,89 @@
1
+ # Optional Apache Arrow integration
2
+
3
+ The core client has no Ruby dataframe dependency. Load the optional adapter explicitly:
4
+
5
+ ```ruby
6
+ require "questdb/arrow"
7
+ ```
8
+
9
+ This adds Arrow C Stream ingestion and query export while loading none of `nanoarrow`, `polars-df`, or `red-arrow`. Install only the consumer you use.
10
+
11
+ ## Ingestion
12
+
13
+ `Client#ingest_arrow` accepts:
14
+
15
+ - any object responding to `arrow_c_stream`, including `Nanoarrow::Array`, `Nanoarrow::ArrayStream`, and `Polars::DataFrame`;
16
+ - `Arrow::Table`, `Arrow::RecordBatch`, and `Arrow::RecordBatchReader` from `red-arrow`.
17
+
18
+ ```ruby
19
+ require "nanoarrow"
20
+ require "questdb/arrow"
21
+
22
+ batch = Nanoarrow::Array.new(
23
+ [
24
+ {"symbol" => "ETH-USD", "price" => 2615.54},
25
+ {"symbol" => "BTC-USD", "price" => 60310.0}
26
+ ],
27
+ Nanoarrow.struct({
28
+ "symbol" => Nanoarrow.string,
29
+ "price" => Nanoarrow.double
30
+ })
31
+ )
32
+
33
+ client.ingest_arrow(
34
+ "trades",
35
+ batch,
36
+ at: QuestDB::TimestampNanos.now,
37
+ symbols: ["symbol"]
38
+ )
39
+ ```
40
+
41
+ `at:` accepts a timestamp column name, `TimestampNanos`, `TimestampMicros`, `Time`, or `SERVER_TIMESTAMP`. Arrow dictionary-encoded UTF-8 defaults to QuestDB SYMBOL and plain UTF-8 defaults to VARCHAR. `symbols:` forces named plain UTF-8 columns to SYMBOL.
42
+
43
+ Use `schema_overrides:` for QuestDB-specific logical types:
44
+
45
+ ```ruby
46
+ client.ingest_arrow(
47
+ "events",
48
+ batch,
49
+ at: :timestamp,
50
+ schema_overrides: {
51
+ venue: :symbol,
52
+ address: :ipv4,
53
+ grade: :char,
54
+ location: [:geohash, 30],
55
+ category: :varchar # force an Arrow dictionary column to VARCHAR
56
+ }
57
+ )
58
+ ```
59
+
60
+ The source stream is consumed exactly once. Arrow producer callbacks and network operations run without the GVL. As required by the Arrow C Data Interface, use a trusted producer whose declared buffer sizes match its allocations; the ABI cannot let a consumer prove buffer lengths. Producer batches are pipelined through the c-client's direct sender and committed at the requested `:ok` or `:durable` boundary. To bound deferred native memory and the replay window, `checkpoint_batches:` defaults to 100; streaming producers retain their own batch sizes. `PooledSender#ingest_arrow` is a convenience forwarding to its client; like `ingest_columns`, it uses a separate direct connection and is not ordered against that sender's row buffer. Standalone WebSocket `QuestDB::Sender#ingest_arrow` opens a poolless direct connection with the same authentication/TLS configuration and likewise has no ordering relationship with its row buffer. If an error reports `in_doubt?`, do not replay the whole stream without suitable QuestDB deduplication keys.
61
+
62
+ ## Query results
63
+
64
+ The neutral integration point is `QueryResult#arrow_c_stream`:
65
+
66
+ ```ruby
67
+ result = client.query("SELECT * FROM trades")
68
+ frame = Polars::DataFrame.new(result)
69
+ ```
70
+
71
+ Convenience methods lazy-load their respective optional gems:
72
+
73
+ ```ruby
74
+ nanoarrow_array = client.query(sql).to_nanoarrow
75
+ polars_frame = client.query(sql).to_polars
76
+ red_arrow_table = client.query(sql).to_red_arrow_table
77
+ ```
78
+
79
+ The current query adapter fully drains the QWP cursor into native Arrow-owned record batches before returning the C Stream capsule. This keeps consumer callbacks nonblocking, permits safe whole-result failover replay, and avoids per-cell Ruby objects, but peak memory is proportional to the result size. Use ordinary `each_batch` or `each_values` when bounded-memory streaming is required.
80
+
81
+ Calling an Arrow materializer consumes and closes the single-use `QueryResult`. SYMBOL columns are exported as compact Arrow dictionaries through the official c-client Arrow path.
82
+
83
+ ## Packaging
84
+
85
+ Arrow support uses the stable Arrow C Data and C Stream interfaces implemented by the pinned `c-questdb-client`. It does not link Arrow C++ or GLib. Arrow Rust sources are included in the existing offline Cargo vendor tree, so the source gem remains fully offline-buildable.
86
+
87
+ Official precompiled extensions include the c-client Arrow feature. This is necessary because Cargo features are selected when the native extension is built, while `require "questdb/arrow"` happens later at runtime; omitting it would make the add-on fail for users of precompiled gems. The module remains optional at the Ruby layer and adds no dataframe dependency. A separate Arrow native gem would duplicate the statically linked c-client and complicate shared ownership, so it is not used.
88
+
89
+ `red-arrow` itself requires Arrow GLib system integration; `nanoarrow` has no runtime gem dependencies. Neither is required by `questdb`.
@@ -0,0 +1,61 @@
1
+ # QWP configuration
2
+
3
+ `QuestDB.connect` accepts either a native `ws::` / `wss::` connection string or `host:` keywords:
4
+
5
+ ```ruby
6
+ QuestDB.connect("ws::addr=localhost:9000;")
7
+ QuestDB.connect(host: "localhost", port: 9000)
8
+ QuestDB.connect(host: "db.example.com", port: 443, tls: true,
9
+ username: ENV.fetch("QDB_USER"),
10
+ password: ENV.fetch("QDB_PASSWORD"))
11
+ ```
12
+
13
+ The forms are mutually exclusive. IPv6 literals may be bare (`::1`) or bracketed (`[::1]`) with `host:`; the builder inserts brackets. Do not include a port in `host:`. Additional keyword settings are converted to the native connect-string grammar. Semicolons are escaped as `;;`; control characters are rejected; a keyword cannot duplicate a key already present in a supplied string.
14
+
15
+ `QuestDB::Client.from_env` reads `QDB_CLIENT_CONF`.
16
+
17
+ `QuestDB::Sender` is the standalone QWP/WebSocket API. It accepts `ws::` or `wss::` through `.from_conf`/`.from_env`, or `protocol: :ws/:wss` with `host:` and `port:`. The default port is 9000. UDP and ILP schemes are rejected.
18
+
19
+ The Ruby-only keywords `connection_listener`, `error_handler`, `connection_event_inbox_capacity`, and `error_event_inbox_capacity` are consumed by the wrapper and are never serialized into the native connect string. They apply to pooled clients and standalone WebSocket senders. Handlers must respond to `call`; capacities must be nonnegative integers. Capacity `0` selects the default 64. A callback-enabled native pool or standalone sender is opened only when the corresponding handler exists. Without `error_handler`, the extension leaves the c-client default rejection logger active.
20
+
21
+ ## Native settings
22
+
23
+ Settings are passed directly to `c-questdb-client` 7.1.0 so new native settings do not require a Ruby release. Important groups include:
24
+
25
+ - pool: `sender_pool_min`, `sender_pool_max`, `query_pool_min`, `query_pool_max`, `acquire_timeout_ms`, `idle_timeout_ms`, `pool_reap`, `lazy_connect`;
26
+ - reconnect/failover: `addr` (comma-separated endpoints), `target`, `zone`, `reconnect_initial_backoff_millis`, `reconnect_max_backoff_millis`, `reconnect_max_duration_millis`, `initial_connect_retry`;
27
+ - store-and-forward: `sf_dir`, `sender_id`, `sf_max_segment_bytes`, `sf_max_total_bytes`, `sf_append_deadline_millis`, `sf_durability`, `sf_sync_interval_millis`, `drain_orphans`, `max_background_drainers`;
28
+ - delivery: `request_durable_ack`, `close_flush_timeout_millis`, `max_buf_size`, `max_name_len`, `error_inbox_capacity`;
29
+ - authentication/TLS: `username`, `password`, `token`, `auth_timeout_ms`, `tls_verify`, `tls_ca`, `tls_roots`, `tls_roots_password`.
30
+
31
+ Consult the matching upstream 7.1.0 connect-string reference for exact constraints. Unknown or invalid settings are rejected by the native parser.
32
+
33
+ Direct column ingestion borrows from the native direct sender pool and uses `acquire_timeout_ms`; initial direct connection retry is bounded by `reconnect_max_duration_millis`. `max_rows_per_batch` is a method argument rather than a connection setting. Direct writes do not use `sf_dir`, do not produce FSNs, and are unaffected by row auto-publish thresholds. `ack: :durable` still requires both `request_durable_ack=on` and server support.
34
+
35
+ ## Auto-publish
36
+
37
+ Pooled row senders default to the Python QWP policy:
38
+
39
+ - 1,000 rows;
40
+ - 100 milliseconds (checked after row appends; no Ruby timer thread);
41
+ - 90% of the native effective frame cap, limited to 8 MiB while no server cap is known.
42
+
43
+ Configure `auto_flush_rows`, `auto_flush_interval`, or `auto_flush_bytes`; use `off` to disable one threshold or `auto_flush=off` to disable all. Auto-publish never waits for an ACK. Explicit `flush(wait: true)` or `wait` provides the barrier.
44
+
45
+ ## Authentication and TLS
46
+
47
+ QWP uses HTTP credentials during the WebSocket upgrade:
48
+
49
+ ```ruby
50
+ QuestDB.connect(
51
+ "wss::addr=db.example.com:443;" \
52
+ "token=#{ENV.fetch('QDB_TOKEN')};" \
53
+ "tls_roots=#{ENV.fetch('QDB_TLS_ROOTS')};"
54
+ )
55
+ ```
56
+
57
+ Use either `username` plus `password` (HTTP Basic) or `token` (bearer), not both. Bearer tokens, multi-host failover, and durable ACKs require the applicable Enterprise server features, but this repository contains no Enterprise server code or credentials.
58
+
59
+ Certificate and hostname verification are enabled by default. `tls_ca` selects `webpki_roots`, `os_roots`, or `webpki_and_os_roots`. For a custom PEM bundle, set `tls_roots=/path/to/ca.pem` and omit `tls_ca`; the native QWP configuration infers PEM mode. With `tls_roots_password`, WSS also accepts JKS/PKCS#12 trust stores. Never commit credentials or log full configuration strings.
60
+
61
+ `tls_verify=unsafe_off` (or `tls_verify: false`) is compiled for isolated diagnostics only. It disables certificate and hostname verification and must not be used in production; install the correct CA instead.
data/docs/design.md ADDED
@@ -0,0 +1,46 @@
1
+ # Design
2
+
3
+ ## Native boundary
4
+
5
+ The gem links statically to official `c-questdb-client` 7.1.0. Ruby owns thin typed-data wrappers for:
6
+
7
+ - one ref-counted `questdb_db` pool state;
8
+ - short-lived borrowed `qwp_sender` leases;
9
+ - caller-owned pool-created `line_sender_buffer` row buffers;
10
+ - native error and structured diagnostic values copied into Ruby objects;
11
+ - synchronous, pool-accounted `qwp_direct_sender` operations and caller-owned `qwp_chunk` values backed by native staging;
12
+ - pooled `qwp_reader`/cursor handles and a standalone `line_sender` for QWP WebSocket.
13
+
14
+ No WebSocket, QWP encoding, TLS, authentication, retry, failover, store-and-forward, or ACK logic is reimplemented in Ruby.
15
+
16
+ ## Ownership
17
+
18
+ The Ruby client and every native sender lease hold a reference to shared pool state. Explicit `Client#close` refuses active leases. If unreachable objects are collected in any order, the pool remains alive until the final lease is returned/dropped, preventing use-after-free. Explicit close/return is idempotent at the Ruby layer and every native pointer is consumed exactly once.
19
+
20
+ Errors that make delivery or connection health uncertain mark the lease for drop rather than recycling it. Client-side validation/configuration errors do not retire a healthy connection. A QWP no-progress timeout from `wait` or the wait phase of `flush(wait: true)` also leaves the lease healthy: queued frames remain owned by store-and-forward and continue delivery. Buffers own their native pointers independently and free them once.
21
+
22
+ Explicit close is the primary lifecycle. As a misuse fallback, an unreachable client or sender enqueues its native handles onto a lock-free deferred cleanup queue. Cleanup records are preallocated with their wrappers, so GC does not allocate them. A postponed Ruby VM job performs sender drop and pool close without the GVL; it never recycles an abandoned sender. The v7 FFI handle wraps Rust's `OwnedSender` (`Send`), so the unreachable handle is moved with a happens-before edge and never used concurrently, despite the stricter public recommendation to return a borrow on its borrowing thread.
23
+
24
+ ## Threading and the GVL
25
+
26
+ The pool supports concurrent borrows, protected against close races by the Ruby client's mutex. Each lease records the borrowing `Thread` and rejects use elsewhere, even without overlapping calls. Native busy flags reject reentrant/concurrent operations.
27
+
28
+ Connect, borrow, return/drop, flush, wait, reap, close, callback-queue waits, and deferred GC cleanup release the GVL. Connect strings are copied before release; row values are encoded into native-owned buffers while the GVL is held. Shared pool references, active-lease counts, closure state, and the pool pointer use C11 atomics so a return/drop cannot race a concurrent close or inspection. Cleanup paths restore busy state after exceptions.
29
+
30
+ Native connection/rejection callbacks never enter Ruby. For pools and standalone WebSocket senders, they deep-copy every borrowed field with plain native allocation and perform a constant-time push into portable mutex/condition-variable queues. The bounded queues use drop-oldest overflow, so slow Ruby handlers never block c-client dispatch, producer, or I/O threads. Dedicated Ruby threads drain the native queues, create immutable values, retain bounded polling copies, and isolate handler exceptions. Delivered/dropped counters add the applicable c-client inbox, native-to-Ruby bridge, and Ruby polling-copy stages; an event can therefore be counted once per stage it successfully exits.
31
+
32
+ Close first atomically rejects work, closes the pool without the GVL (fencing c-client dispatchers), then closes/wakes bridge queues. Ruby dispatcher joins happen outside the client mutex and skip the calling dispatcher when a callback closes its own client. Failed connect tears down its fenced callback context. Deferred native cleanup performs the same fence without calling Ruby.
33
+
34
+ ## Row atomicity
35
+
36
+ Every row starts with a native marker. Any Ruby exception or native validation error rewinds to it before propagating. This prevents partially encoded rows from contaminating a later publish.
37
+
38
+ ## Direct column ownership
39
+
40
+ Ruby synchronously normalizes direct columns into private immutable packed values. The extension then deep-copies all packed data, names, offsets, symbol dictionaries, validity maps, tensor shapes, and designated timestamps into native allocations before releasing the GVL. Callers must not mutate inputs concurrently just because the method has started; mutation is safe after the call returns or after a separately constructed `ColumnVector` has copied its values. A `qwp_chunk` borrows native allocations only while the direct flush is in flight. All batches share one borrowed direct sender and are committed once; successful commit returns it, while any uncertain or partial path drops it. A temporary pool-state reference and active-lease count prevent concurrent client close and are released exactly once, including asynchronous Ruby exceptions.
41
+
42
+ Direct ingestion does not enter the row sender's store-and-forward queue and cannot be ordered relative to it. Its only success boundary is the checked direct commit. After one or more flushes, errors are conservatively marked in-doubt and include a partial-completion warning.
43
+
44
+ ## Current boundary
45
+
46
+ The client exposes pooled QWP row ingress, native-staged direct columnar ingress, QWP egress queries, pool-wide Ruby callback dispatch, and standalone QWP WebSocket ingestion. Reader batches are copied into immutable Ruby objects before advancing the native cursor, and partial-result cleanup drops rather than recycles the connection. Whole-result materialization clears a partial prefix after a failover reset; streaming raises before replay can duplicate yielded rows. ILP compatibility is not retained. The c-client Arrow feature is linked, but Ruby-facing Arrow methods are installed only by `require "questdb/arrow"`; no dataframe gem is a core dependency.
data/docs/ingestion.md ADDED
@@ -0,0 +1,96 @@
1
+ # Pooled QWP row ingestion
2
+
3
+ Borrow a sender for one logical unit of work and return it promptly:
4
+
5
+ ```ruby
6
+ client.sender do |sender|
7
+ sender.row("telemetry",
8
+ symbols: { device: "sensor-7" },
9
+ columns: { temperature: 21.5, active: true },
10
+ at: QuestDB::TimestampNanos.now)
11
+ sender.flush(wait: true)
12
+ end
13
+ ```
14
+
15
+ `symbols` become QuestDB `SYMBOL` columns; `columns` carry scalar, timestamp, decimal, or `Float64Array` values. `nil` values are omitted. Plain Ruby arrays are rejected to avoid ambiguous type/shape inference. A row containing only omitted values is a no-op. Timestamp wrappers represent the full signed 64-bit QWP domain for query values, but row/designated timestamps still must satisfy the native QWP ingestion rules; invalid pre-epoch row timestamps are rejected atomically by the native buffer API.
16
+
17
+ Every row append uses a native bookmark. If Ruby conversion or native validation fails, the entire row is rewound and earlier completed rows remain usable.
18
+
19
+ ## Publishing and acknowledgement
20
+
21
+ - `flush` publishes and clears buffered rows locally.
22
+ - `flush(wait: true)` additionally waits for the `OK` acknowledgement level.
23
+ - `wait(timeout_millis: 0)` waits for all frames published through this lease; zero means no no-progress deadline.
24
+ - `flush_and_get_fsn` returns the frame sequence number assigned to a publication.
25
+ - `flush_and_keep_and_get_fsn` publishes without clearing the row buffer.
26
+ - `published_fsn` and `acked_fsn` expose connection watermarks.
27
+ - `await_acked_fsn(fsn, timeout_millis:)` returns false on no-progress timeout.
28
+ - `ack: :durable` requires native `request_durable_ack=on` and server support.
29
+
30
+ FSNs belong to the borrowed connection and are meaningful only while that lease is held. `acked_fsn` can advance for a frame dropped after a non-terminal server rejection; inspect diagnostics as well.
31
+
32
+ ## Diagnostics
33
+
34
+ `poll_error` returns an immutable `QuestDB::SenderError` with category, applied policy, optional status, message, optional message sequence, and affected FSN range. `error_events_dropped` reports overflow of that connection's bounded diagnostic ring.
35
+
36
+ Pool-wide server rejections use the native non-silent default logger. `Client#error_events_delivered` and `#error_events_dropped` expose pool counters.
37
+
38
+ ## Standalone QWP senders
39
+
40
+ `QuestDB::Sender` owns one QWP WebSocket connection. Its row API matches pooled senders, but it is not part of a deployment pool. WebSocket senders provide `flush`, ACK waits, FSNs, diagnostics, manual `drive_once`, checked close-drain, direct columnar ingestion, and safely queued `connection_listener` / `error_handler` callbacks. Standalone handlers run on dispatcher threads and must not call the thread-affine sender; communicate back to its owner thread instead. Configure `qwp_ws_progress=manual` when the application will drive progress itself. Standalone auto-flush defaults to 600 rows and 1,000 ms, matching the Python WebSocket client. Standalone senders are thread-affine and should be explicitly closed or used with `open`.
41
+
42
+ ## Concurrency
43
+
44
+ `QuestDB::Client` can serve concurrent Ruby threads. A `PooledSender` and its internal buffer are single-threaded and bound to the borrowing Ruby thread. Use one lease per thread:
45
+
46
+ ```ruby
47
+ threads = 4.times.map do |worker|
48
+ Thread.new do
49
+ client.sender do |sender|
50
+ sender.row("jobs", symbols: { worker: worker.to_s },
51
+ columns: { done: true }, at: QuestDB::SERVER_TIMESTAMP)
52
+ end
53
+ end
54
+ end
55
+ threads.each(&:join)
56
+ ```
57
+
58
+ Blocking native connect, borrow, publish/wait, return/drop, reap, and close calls run without the GVL. Ruby strings and arrays are converted before any no-GVL operation.
59
+
60
+ ## Direct columnar ingestion
61
+
62
+ `Client#ingest_columns` (alias `write_columns`) accepts equal-length column arrays and uses the pool's direct QWP sender:
63
+
64
+ ```ruby
65
+ client.ingest_columns(
66
+ "telemetry",
67
+ columns: {
68
+ temperature: [21.5, nil, 19.75],
69
+ sequence: QuestDB::ColumnVector.int([1, 2, 3])
70
+ },
71
+ symbols: { device: ["a", "a", "b"] },
72
+ at: [
73
+ QuestDB::TimestampNanos.new(1_700_000_000_000_000_001),
74
+ QuestDB::TimestampNanos.new(1_700_000_000_000_000_002),
75
+ QuestDB::TimestampNanos.new(1_700_000_000_000_000_003)
76
+ ],
77
+ max_rows_per_batch: 16_384,
78
+ ack: :ok
79
+ )
80
+ ```
81
+
82
+ The same API is available as `PooledSender#ingest_columns` and on standalone `ws`/`wss` senders. The pooled form borrows a separate direct connection from its client; the standalone form opens a poolless direct connection from the sender's auth/TLS configuration. Neither form orders the direct commit against that sender's row buffer.
83
+
84
+ Homogeneous Arrays infer conservative types (`Integer`→LONG, `Float`→DOUBLE, `String`→VARCHAR, plus the explicit QuestDB wrapper classes). All-null arrays and ambiguous or heterogeneous values require `ColumnVector`. Explicit kinds are `byte`, `short`, `int`, `long`, `float`, `double`, `boolean`, `varchar`, `binary`, `uuid`, `long256`, `ipv4`, `timestamp`, `timestamp_nanos`, `date`, `char`, `geohash`, `decimal64`, `decimal128`, `decimal256`, and `double_array`. GEOHASH requires `precision_bits:`, decimals require `scale:`, and DOUBLE_ARRAY requires a common `shape:`. SYMBOL arrays go in `symbols:`.
85
+
86
+ `at:` is required and accepts a non-null Array of `TimestampNanos`, `TimestampMicros`, or `Time`, a fixed timestamp value, or `SERVER_TIMESTAMP`. A fixed value is repeatable and suitable for QuestDB deduplication. Server timestamps are assigned on arrival and therefore change on retry. Empty columns are a no-op. Names, UTF-8, lengths, ranges, shapes, timestamp nullability, ACK level, and batch size are validated before a native sender is borrowed.
87
+
88
+ The implementation synchronously normalizes inputs and then copies packed values, offsets, dictionaries, validity bitmaps, names, and timestamps into native-owned staging before releasing the GVL. Do not mutate input Arrays or strings concurrently merely because the method has started; they are safe to mutate after `ingest_columns` returns (or after a separately constructed `ColumnVector` has copied them). Each chunk borrows staging only until its direct flush returns.
89
+
90
+ ### Direct commit semantics
91
+
92
+ Direct ingestion pipelines every batch on one direct connection and performs one checked commit at `ack: :ok` or `:durable`. It is intentionally **not** store-and-forward: there are no FSNs, no background replay, and returning a direct connection is not used as an unchecked commit. On failure the connection and uncommitted tail are dropped. If any batch was flushed before an error, the raised `QuestDB::Error` says that partial completion is possible and `in_doubt?` is true; never retry the whole input blindly. Use stable designated timestamps plus QuestDB deduplication when application retries are required.
93
+
94
+ Row and direct-column writes use different delivery paths. They have no ordering relationship, even when issued through the same `Client`; establish ordering with application-level acknowledgement/query barriers rather than call order.
95
+
96
+ QuestDB BYTE, SHORT, BOOLEAN, and CHAR have no distinct SQL NULL representation. Their direct vectors reject `nil`; nullable kinds preserve `nil` with validity bitmaps. INT/LONG minimum sentinels and floating NaN are likewise rejected as non-null values. Native Arrow C Stream ingestion is available through the optional [`questdb/arrow`](arrow.md) adapter.
@@ -0,0 +1,36 @@
1
+ # Performance
2
+
3
+ The hot row path crosses the Ruby/C boundary once per row and calls the official native buffer API per field. Blocking pool/network operations release the GVL. For throughput:
4
+
5
+ - keep one `QuestDB::Client` for the process/service;
6
+ - borrow one sender per worker thread or logical batch;
7
+ - keep auto-publish enabled unless the application owns batching;
8
+ - avoid `flush(wait: true)` per row—publish batches, then use one ACK barrier;
9
+ - use `sf_dir` when reconnect/replay durability matters;
10
+ - size `sender_pool_max` to application concurrency and server connection capacity;
11
+ - use `ingest_columns` for already column-oriented batches and tune `max_rows_per_batch` to balance staging/frame size against per-chunk overhead;
12
+ - for allocation-critical row producers, reuse the table name and mutable `symbols`/`columns` Hashes between calls—the normal keyword API remains unchanged;
13
+ - use `QueryResult#each_values` when positional Array access is sufficient; ordinary `each` retains ergonomic indexed `Row` objects and name lookup.
14
+
15
+ `Buffer#bytesize` and `#capacity` are QWP implementation hints, not serialized wire byte counts. Benchmark labels deliberately distinguish row buffering from QWP publication and ACK latency.
16
+
17
+ Run:
18
+
19
+ ```sh
20
+ bundle exec ruby -Ilib benchmark/serialize_rows.rb
21
+ bundle exec ruby -Ilib benchmark/preparation.rb
22
+ QDB_BENCH_CONF='ws::addr=localhost:9000;auto_flush=off;' \
23
+ bundle exec ruby -Ilib benchmark/network.rb
24
+ QDB_BENCH_CONF='ws::addr=localhost:9000;' \
25
+ bundle exec ruby -Ilib benchmark/query_rows.rb
26
+ QDB_BENCH_CONF='ws::addr=localhost:9000;' \
27
+ bundle exec ruby -Ilib benchmark/query_access.rb
28
+ QDB_BENCH_CONF='ws::addr=localhost:9000;' \
29
+ ruby -Ilib benchmark/arrow.rb # optional nanoarrow gem
30
+ QDB_BENCH_CONF='ws::addr=localhost:9000;' \
31
+ bundle exec ruby -Ilib benchmark/columnar.rb
32
+ QDB_BENCH_CONF='ws::addr=localhost:9000;auto_flush=off;' \
33
+ bundle exec ruby -Ilib benchmark/ingestion.rb
34
+ ```
35
+
36
+ Reports contain medians, median absolute deviation, allocation counts, raw samples, and environment metadata. The `ingestion` benchmark times complete row build/publish/ACK and direct columnar build/commit paths, while `network` deliberately excludes row construction. Run `rake benchmark:smoke` for a broad catastrophic-regression guard. Set `BASELINE=baseline.json` when running `ingestion.rb`, or use `benchmark/check.rb`, to enforce median-throughput and allocation budgets against a like-for-like dedicated-runner baseline. Shared-host results are not defensible regression thresholds.
@@ -0,0 +1,34 @@
1
+ # Python QWP parity
2
+
3
+ This client tracks the QWP surface of the QuestDB Python 5.0 client at the `c-questdb-client` 7.1.0 ABI boundary. Parity means equivalent delivery, lifecycle, type, and error semantics expressed idiomatically in Ruby; it does not mean retaining Python's legacy ILP APIs.
4
+
5
+ | Python QWP capability | Ruby API | Status |
6
+ | --- | --- | --- |
7
+ | Deployment connection and shared sender/reader pools | `QuestDB.connect`, `Client` | Equivalent |
8
+ | Thread-affine pooled row sender, auto-publish, ACK barriers | `Client#sender`, `PooledSender` | Equivalent |
9
+ | FSNs, published/acked watermarks, durable ACK | `flush_and_get_fsn`, `await_acked_fsn`, `ack: :durable` | Equivalent |
10
+ | Store-and-forward, failover, TLS and authentication | Official connect-string settings | Native pass-through |
11
+ | Direct columnar/dataframe load | `Client#ingest_columns`, `ColumnVector`; optional `Client#ingest_arrow` | Ruby-native arrays plus Arrow C Stream integration |
12
+ | SQL query/execute and reusable reader lease | `Client#query`, `#execute`, `#reader` | Equivalent |
13
+ | Typed positional binds and scalar/`DOUBLE[]` results | Ruby value wrappers, `Row`, `ResultBatch` | Equivalent within the v7 typed reader ABI |
14
+ | Arrow query export | Optional `QueryResult#arrow_c_stream` and dataframe conveniences | Equivalent native C Stream boundary; currently whole-result materialized |
15
+ | Query cancellation, single-use results, failover reset | `QueryResult` | Equivalent; streaming refuses duplicate replay |
16
+ | Pool connection/rejection callbacks | `connection_listener`, `error_handler` | Equivalent with safer queued Ruby dispatch |
17
+ | Standalone WebSocket sender and manual progress | `QuestDB::Sender` with `ws::`/`wss::` | Equivalent |
18
+ | Standalone callback diagnostics | `Sender` callback keywords and polling copies | Equivalent with queued Ruby dispatch |
19
+ | QWP/UDP datagrams | Not exposed | Intentional scope reduction; use WebSocket |
20
+ | Structured native errors and delivery ambiguity | `Error#code`, `#sender_error`, `#in_doubt?` | Equivalent |
21
+ | Standalone WebSocket auto-flush defaults | 600 rows, 1,000 ms | Matched |
22
+
23
+ ## Intentional Ruby-native differences
24
+
25
+ - Homogeneous Ruby arrays and immutable `ColumnVector` descriptors are the dependency-free columnar API. `require "questdb/arrow"` adds native Arrow C Stream support for Nanoarrow, Polars, and Red Arrow without requiring any of them.
26
+ - Arrow query export currently drains into native-owned Arrow batches before returning the stream capsule; use Ruby `each_batch`/`each_values` for bounded-memory streaming. A call targets one table; split multi-table input explicitly.
27
+ - Python's ILP TCP/HTTP compatibility, protocol v1, HTTP transactions, and best-effort QWP/UDP transport are outside this WebSocket-only client's scope.
28
+ - Ruby callback handlers run on dedicated Ruby threads. Native callback threads only deep-copy into bounded queues and never invoke Ruby.
29
+ - Standalone `length` is the number of buffered QWP rows, matching pooled senders, rather than the legacy ILP byte-count convention. `Buffer#bytesize` remains a native memory hint.
30
+ - `LONG_ARRAY` query projection is rejected explicitly because c-client 7.0 does not expose a typed LONG-array projection. `DOUBLE_ARRAY` is fully supported.
31
+
32
+ ## Parity guardrails
33
+
34
+ Unit and live tests cover row and direct ingestion, standalone WebSocket, callbacks, typed binds/results, failover replay policy, native lifecycle/GC, ACK barriers, error diagnostics, and thread affinity. The benchmark suite covers local row buffering, complete row ingestion, direct columnar commits, query decoding, allocations, buffer growth, and shared-pool concurrency. See [performance](performance.md) for regression-baseline usage.
data/docs/querying.md ADDED
@@ -0,0 +1,54 @@
1
+ # QWP queries
2
+
3
+ `QuestDB::Client#query` borrows a QWP reader from the shared native pool and returns a single-use `QuestDB::QueryResult`. Use the block form or close the result explicitly.
4
+
5
+ ```ruby
6
+ client.query("SELECT symbol, price FROM trades WHERE price > $1", binds: [100.0]) do |result|
7
+ result.each do |row|
8
+ puts [row["symbol"], row["price"]].inspect
9
+ end
10
+ end
11
+ ```
12
+
13
+ `QueryResult#each_batch` yields immutable `ResultBatch` objects. Every batch, column name, and value has been copied into Ruby-owned memory before the native cursor can advance. `each` yields immutable `Row` objects. Rows retain the complete ordered name list, including duplicates; name lookup returns the first match and `to_h` necessarily keeps only the final value for duplicate names. Name lookup uses a batch-shared index, so `row[:symbol]` remains ergonomic without a per-access String allocation.
14
+
15
+ For allocation-sensitive scans that only need positional values, `each_values` yields frozen, Ruby-owned Arrays directly and avoids allocating a `Row` wrapper:
16
+
17
+ ```ruby
18
+ result.each_values { |values| consume(values[0], values[1]) }
19
+ ```
20
+
21
+ Whole-result `to_a` safely handles a mid-query failover by discarding its partial prefix before accepting replay. Streaming `each`, `each_values`, and `each_batch` cannot retract rows already yielded, so they raise `:failover_would_duplicate` before yielding replayed rows.
22
+
23
+ For native interchange with Nanoarrow, Polars, or Red Arrow, load `questdb/arrow`. Its C Stream query path avoids per-cell Ruby objects; see [optional Arrow integration](arrow.md).
24
+
25
+ For several sequential queries on one connection, borrow a reader:
26
+
27
+ ```ruby
28
+ client.reader do |reader|
29
+ reader.execute("CREATE TABLE IF NOT EXISTS audit (ts TIMESTAMP, n LONG) TIMESTAMP(ts) WAL")
30
+ rows = reader.query("SELECT n FROM audit ORDER BY ts").to_a
31
+ end
32
+ ```
33
+
34
+ Only one result may be active on a reader. Fully draining a result returns a healthy connection to the pool. `cancel` synchronously requests cancellation and drains to a terminal boundary; call `close` afterward. Closing an undrained result marks the connection unsafe to recycle and terminates that reader lease. Results and readers are thread-affine.
35
+
36
+ ## Binds
37
+
38
+ Binds are positional and correspond to `$1`, `$2`, and so on. The container must be an `Array`. Supported values:
39
+
40
+ - `nil` (NULL VARCHAR), `true`/`false`, signed 64-bit `Integer`, `Float`, and UTF-8 `String`
41
+ - `Time`, `TimestampMicros`, `TimestampNanos`, and `DateMillis`; timestamp wrappers use the full signed 64-bit range, so pre-epoch query values are preserved exactly
42
+ - `BigDecimal` or `DecimalValue`
43
+ - `UUID`, `Binary`, `IPv4`/`IPAddr`, `Geohash`, `Long256`, and `Char`
44
+ - `TypedNull` when SQL requires a non-VARCHAR NULL type
45
+
46
+ Values are validated and converted before query execution. Decimal mantissas retain arbitrary precision up to QWP DECIMAL256. The client can encode BINARY and IPv4 values (including `TypedNull.new(:binary)` and `TypedNull.new(:ipv4)`), but current Phase-1 QuestDB servers reject those bind kinds; this is a server limitation rather than local coercion.
47
+
48
+ ## Result types
49
+
50
+ Numeric and boolean columns become Ruby numeric/boolean values. TIMESTAMP and TIMESTAMP_NS use the exact integer timestamp wrappers; DATE uses `DateMillis`. Decimal values use `DecimalValue`, UUID uses `UUID`, IPv4 uses `IPv4`, LONG256 uses `Long256`, GEOHASH uses `Geohash`, CHAR uses `Char`, and BINARY is an `ASCII-8BIT` String. SYMBOL and VARCHAR are UTF-8 strings. DOUBLE arrays become `Float64Array` with their per-row shape. SQL NULL always becomes `nil`.
51
+
52
+ A zero-row QWP batch still establishes `QueryResult#columns`, preserving the result schema. `LONG_ARRAY` cannot be projected by the c-questdb-client 7.0 reader API, so materialization raises an explicit unsupported-column error instead of fabricating cell values (including for NULL cells). Future unknown wire kinds likewise raise rather than being guessed or silently corrupted; `OpaqueValue` remains available for a future native projection that can provide the original bytes.
53
+
54
+ QWP query execution does not make a preceding WAL write immediately visible. An ingestion `OK` acknowledgement means QuestDB accepted the frame; WAL application remains asynchronous.
@@ -0,0 +1,70 @@
1
+ # Infrastructure for the initial 0.1.0 release
2
+
3
+ This is an inventory for the early-adopter release, not authorization to run builds or publish. Existing approved hosts are sufficient; dedicated Azure/Hetzner automation is not technically required. `docs/releasing.md` currently specifies Azure/Hetzner agents, so using another provider requires a release-owner-approved policy change. No release jobs have been started by preparing this inventory.
4
+
5
+ ## Required for the advertised eight-gem release
6
+
7
+ ### Build and install-test hosts
8
+
9
+ Five host classes cover the seven native platforms. They can be reused sequentially; 28 simultaneously provisioned machines are not needed.
10
+
11
+ | Host | Native gem targets | Environment |
12
+ | --- | --- | --- |
13
+ | Linux x86-64 | `x86_64-linux-gnu`, `x86_64-linux-musl` | Docker; manylinux_2_28_x86_64 for GNU builds; Ruby Alpine containers for musl |
14
+ | Linux ARM64 | `aarch64-linux-gnu`, `aarch64-linux-musl` | Docker; manylinux_2_28_aarch64 for GNU builds; ARM64 Ruby Alpine containers for musl |
15
+ | macOS Intel | `x86_64-darwin` | Apple command-line tools; deployment target macOS 12.0 |
16
+ | macOS Apple Silicon | `arm64-darwin` | Apple command-line tools; deployment target macOS 12.0 |
17
+ | Windows x64 | `x64-mingw-ucrt` | RubyInstaller UCRT/MSYS2 development tools; Rust `x86_64-pc-windows-gnu` target |
18
+
19
+ On each target:
20
+
21
+ - MRI Ruby 3.1, 3.2, 3.3, and 3.4, appropriate Ruby development headers, Bundler, and Make/C compiler.
22
+ - Rust 1.91.1 or newer, with the release toolchain recorded.
23
+ - Binary stripping/linkage inspection tools; GNU builds must enforce the glibc 2.28 symbol ceiling.
24
+ - Clean install-test environments for all four Ruby minors. Test the final gem outside the checkout so local extensions cannot mask missing package contents.
25
+ - RubyGems 3.3.22+ for flavor-specific Linux packages; Bundler 2.5.6+ for musl selection.
26
+ - Network access to GitHub, RubyGems, crates.io, and required image/toolchain registries during preparation. Source-gem validation must also run with Cargo networking disabled.
27
+
28
+ This produces 28 versioned extensions, packaged into seven platform gems, plus one source gem. One Linux host can assemble and verify all eight packages and perform the source-gem offline install test. Handle the source gem's Ruby runtime dependencies explicitly; offline Cargo compilation does not imply offline Ruby dependency resolution. Native install tests should preserve installed runtime gems rather than accidentally demanding a compiler to reinstall `bigdecimal`.
29
+
30
+ ### Qualification environment
31
+
32
+ The x86-64 Linux host can also provide:
33
+
34
+ - Docker and the pinned QuestDB 10.0.1 OSS image for live QWP ingestion, queries, callbacks, and Arrow tests.
35
+ - Two independently reachable QuestDB endpoints for the existing query-failover qualification script.
36
+ - Disposable Docker volumes and local store-and-forward directories; permission to kill and restart only the qualification containers.
37
+ - Nanoarrow and Red Arrow test dependencies, including Arrow GLib/GObject development packages.
38
+ - Valgrind for the checked-in native lifecycle tests.
39
+ - `bundler-audit`, `cargo-audit`, and an SPDX-capable SBOM generator.
40
+ - Repository/package secret and path scanning, plus the checked-in license-header and package-verification scripts.
41
+
42
+ No production database, Grafana deployment, or dedicated benchmark host is needed. Enterprise durable-ACK qualification is optional under the current initial-release policy. Shared-host benchmark results are informational.
43
+
44
+ ### Artifact retention
45
+
46
+ An access-controlled artifact directory/store is sufficient; it need not be a new service. Retain:
47
+
48
+ - The eight final gem files and sorted `SHA256SUMS`.
49
+ - SPDX SBOM and build provenance: candidate SHA, recursive submodule SHAs, toolchains, runner/container identities, build commands, and artifact hashes.
50
+ - Dependency-audit, unit/live/Arrow, native-memory, fault, and install-test results for the same candidate.
51
+ - Release-owner approval and any explicit security exceptions.
52
+
53
+ Publish these exact approved files without rebuilding them. Do not include credentials, Enterprise sources, private keys, or proprietary/local paths.
54
+
55
+ ### Publishing access
56
+
57
+ - An approved RubyGems maintainer account with MFA and permission to claim/publish `questdb`; configure official QuestDB ownership.
58
+ - A least-privilege RubyGems API key held only in the publisher's protected user-level credentials file (owner-only permissions) for manual publication. Never put it in source, logs, commands, or release artifacts.
59
+ - An approved secure publishing workstation with RubyGems, checksum tools, Git, and a signing identity for the `v0.1.0` tag.
60
+ - GitHub permissions to push the approved signed tag and create a release with the final artifacts and notes.
61
+ - Representative GNU, musl, macOS, and Windows environments for post-publication installation checks; reuse the hosts above.
62
+
63
+ ## Can be deferred
64
+
65
+ - Dedicated Azure/Hetzner pipeline migration, if an alternative is explicitly approved.
66
+ - Automated publishing and RubyGems trusted-publishing integration.
67
+ - Dedicated benchmark hardware and performance dashboards.
68
+ - Enterprise-only durable-ACK test infrastructure.
69
+
70
+ If all five host classes are unavailable, a source-only or reduced-platform initial release is possible only after explicitly narrowing the release policy and advertised precompiled-platform support. Do not silently omit promised packages.
data/docs/releasing.md ADDED
@@ -0,0 +1,47 @@
1
+ # Releasing
2
+
3
+ Releases contain one checksum-complete source gem and precompiled native gems for every supported platform. Release builds run only on approved QuestDB Azure Pipelines and Hetzner agents; preparing a candidate never authorizes running or publishing it.
4
+
5
+ ## Supported native gem matrix
6
+
7
+ Each platform gem carries a versioned extension for MRI 3.1, 3.2, 3.3, and 3.4:
8
+
9
+ - `x86_64-linux-gnu` and `aarch64-linux-gnu` (built and symbol-gated on glibc 2.28);
10
+ - `x86_64-linux-musl` and `aarch64-linux-musl`;
11
+ - `x86_64-darwin` and `arm64-darwin`;
12
+ - `x64-mingw-ucrt`.
13
+
14
+ The versioned layout is required because MRI's native ABI and `libruby` soname may change between minor releases. `lib/questdb/ingress/native.rb` selects the extension matching the running Ruby minor. Flavor-specific Linux packages require RubyGems 3.3.22 or newer, and musl users should use Bundler 2.5.6 or newer so the correct libc variant is selected.
15
+
16
+ ## Candidate validation
17
+
18
+ 1. Update `QuestDB::Ingress::VERSION` and change the changelog heading from `unreleased` to the intended release date.
19
+ 2. Build from a clean candidate commit with recursively initialized submodules on approved QuestDB infrastructure.
20
+ 3. Require Ruby and Rust dependency audits to pass without ignored advisories. Temporary exception for 0.1.0: the rustls advisories listed in `.cargo/audit.toml` are ignored until a patch release moves `c-questdb-client` to rustls 0.23.46 or newer; remove each ignore as soon as its fix ships. Enterprise-only durable-ACK qualification remains optional, matching the Python client's policy.
21
+ 4. Run unit, integration, Arrow, native-memory, package, and bounded fault qualification. Shared-host benchmark results remain informational.
22
+ 5. Produce all eight gems, then run `script/verify-gem` on each final file and inspect native linkage. Clean-install each native gem on MRI 3.1, 3.2, 3.3, and 3.4 on its matching platform.
23
+ 6. Confirm the source gem installs with `CARGO_NET_OFFLINE=true` in a clean `GEM_HOME` and contains no generated extension or nested QuestDB server source.
24
+ 7. Generate sorted `SHA256SUMS`, an SPDX SBOM, and build provenance from the final files.
25
+ 8. Confirm no credentials, private keys, Enterprise sources, proprietary paths, or local paths are present.
26
+ 9. Have a release owner approve the immutable artifact set before creating a tag or pushing any gem.
27
+
28
+ ## Initial 0.1 release policy
29
+
30
+ `0.1.0` is a normal RubyGems version so users can install it without `--prerelease`. It is described as an initial pre-1.0 release in the README, changelog, and release notes. Use a version such as `0.1.0.beta.1` instead only if RubyGems should treat the package as a formal prerelease.
31
+
32
+ ## Publishing
33
+
34
+ 1. For the initial release, an approved QuestDB maintainer account with MFA enabled may claim the unowned `questdb` name. Add or transfer ownership to the official QuestDB maintainers as soon as that account is available.
35
+ 2. Create a signed `vVERSION` tag pointing to the exact candidate commit and verify the tag before publishing.
36
+ 3. Promote the already validated artifacts; do not rebuild different files in the publishing stage.
37
+ 4. Manual publication from an approved secure workstation is acceptable. Keep the least-privilege RubyGems key only in RubyGems' user-level credentials file, with owner-only filesystem permissions; do not pass it through an environment variable or command-line argument.
38
+ 5. From a directory outside the checkout, verify `SHA256SUMS`, then push the source gem followed by each of the seven platform gems. Let `gem push` prompt interactively for the MFA code.
39
+ 6. Push all platform variants for the same version only after the complete set has passed validation. RubyGems does not permit replacing a published file in place.
40
+ 7. Create the GitHub release with release notes, `SHA256SUMS`, SPDX SBOM, provenance, and the eight gem files.
41
+ 8. Install from RubyGems on representative GNU, musl, macOS, and Windows hosts and add official owners with `gem owner questdb --add OWNER` as required by QuestDB policy.
42
+
43
+ Publishing credentials must never be written to the repository, logs, artifacts, shell history, connection strings, or release bundles. Revoke or rotate the initial key after ownership and long-term release access are configured.
44
+
45
+ ## Arrow binary policy
46
+
47
+ Official extensions always compile the pinned c-client's `arrow` feature. `require "questdb/arrow"` is optional and no Arrow/dataframe gem is a runtime dependency, but Cargo features cannot be enabled after a precompiled extension is installed. Splitting Arrow into another native gem would statically link a second c-client and complicate ownership across extensions, so the modestly larger official binary is preferred.
@@ -0,0 +1,41 @@
1
+ # Troubleshooting
2
+
3
+ ## `config_error`
4
+
5
+ Only `ws::` and `wss::` are accepted. Check duplicate keys, escaped semicolons (`;;`), pool limits, TLS options, and store-and-forward paths. Configuration is parsed by the pinned native client.
6
+
7
+ ## `auth_error`, `tls_error`, or `handshake_error`
8
+
9
+ QWP authenticates during the WebSocket HTTP upgrade. Use either Basic (`username`/`password`) or bearer `token`. For private PKI, configure `tls_roots`; do not disable verification in production. QuestDB QWP requires server 10.0+.
10
+
11
+ ## `failover_retry` and `in_doubt?`
12
+
13
+ A `failover_retry` from `wait` or the acknowledgement phase of `flush(wait: true)` can be a no-progress timeout. It does not discard the queued frame or retire the sender; the background store-and-forward runner keeps delivering, so call `wait`/`await_acked_fsn` again rather than publishing the same rows again. Other failures follow the native reconnect/failover policy. If `error.in_doubt?` is true, replaying the same rows may duplicate data. Use designated client timestamps and appropriate QuestDB deduplication keys.
14
+
15
+ ## Direct column write failed or may be partial
16
+
17
+ Direct column writes are not store-and-forward. Batches are pipelined and then committed at one checked ACK boundary. If an error follows one or more successful direct flushes, its message warns about partial completion and `in_doubt?` is true. Do not retry the complete Array set blindly; use stable designated timestamps with QuestDB deduplication, or reconcile the written timestamp/key range first. A pool/acquire/connect failure before the first flush is safe to retry only when `in_doubt?` is false.
18
+
19
+ Length, type, UTF-8, range, shape, or `at:` errors are local and occur before borrowing. All-null columns need an explicit `ColumnVector`. BYTE, SHORT, BOOLEAN, and CHAR reject `nil` because QuestDB cannot represent a distinct NULL for those types. Row and direct ingestion have no cross-path ordering guarantee.
20
+
21
+ ## A flush succeeded but rows are not queryable
22
+
23
+ Publish-only `flush` confirms only local store-and-forward acceptance. `flush(wait: true)` confirms server acceptance, not WAL application. Poll query visibility with a bounded deadline when read-after-write is required.
24
+
25
+ ## Server rejection appeared in logs
26
+
27
+ Without `error_handler`, pool-wide diagnostics use the native default logger so rejections are never silent. With a handler, it receives an immutable `SenderError` on a dedicated Ruby thread; `Client#poll_error` returns a nonblocking polling copy. A pooled sender can already have been returned or closed when its pool-wide diagnostic arrives. Check `error_events_dropped` and the pool delivered/dropped counters if diagnostics arrive faster than they are consumed.
28
+
29
+ Callback exceptions are reported with `warn` and do not stop dispatch or poison the client. Connection events and rejections are diagnostic signals, not ACK or WAL-visibility barriers. Slow handlers can overflow bounded drop-oldest queues; increase the matching Ruby-only inbox capacity or keep handlers lightweight. If a callback closes its client, dispatch shuts down without joining itself. A handler that captures the client keeps it alive, so use explicit `close`.
30
+
31
+ ## Partial or abandoned query results
32
+
33
+ Fully iterate, cancel, or close every `QueryResult`. Closing an undrained result intentionally terminates its reader lease; the connection is not returned to the pool because unread protocol frames would contaminate the next query. Use `Client#query` with a block for automatic cleanup. A result is single-use.
34
+
35
+ ## `client is busy`, pooled lease errors, or thread errors
36
+
37
+ Do not share sender, reader, or query-result leases across threads. Borrow one lease per thread. Client pool methods may be called concurrently through the Ruby wrapper, but closing a client with active leases is rejected. Always use block forms or explicit `close`; GC cleanup is a deferred emergency path that drops abandoned handles instead of recycling them.
38
+
39
+ ## Offline native build fails
40
+
41
+ Use Rust 1.91.1 or newer. Confirm `vendor/c-questdb-client` is at the revision in `vendor/c-questdb-client.version`, initialize submodules, and keep `.cargo/config.toml` pointing to `vendor/rust-dependencies`. Cargo checksum errors indicate a modified or incompletely packaged vendored source tree.
data/examples/basic.rb ADDED
@@ -0,0 +1,32 @@
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(host: "localhost") do |client|
25
+ client.sender do |sender|
26
+ sender.row("trades",
27
+ symbols: { symbol: "ETH-USD", side: "sell" },
28
+ columns: { price: 2615.54, amount: 0.00044 },
29
+ at: QuestDB::TimestampNanos.now)
30
+ sender.flush(wait: true)
31
+ end
32
+ end