questdb 0.1.0 → 0.1.1

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/audit.toml +17 -9
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +1 -1
  5. data/THIRD_PARTY_NOTICES.md +2 -2
  6. data/docs/configuration.md +3 -2
  7. data/docs/design.md +1 -1
  8. data/docs/python-parity.md +1 -1
  9. data/docs/releasing.md +1 -1
  10. data/ext/questdb_ingress/extconf.rb +2 -0
  11. data/ext/questdb_ingress/questdb_ingress.c +2 -1
  12. data/ext/questdb_ingress/unlink_build_ruby.rb +58 -0
  13. data/lib/questdb/arrow.rb +2 -0
  14. data/lib/questdb/client.rb +6 -0
  15. data/lib/questdb/config.rb +19 -0
  16. data/lib/questdb/pooled_sender.rb +4 -1
  17. data/lib/questdb/sender.rb +10 -1
  18. data/lib/questdb/version.rb +2 -2
  19. data/script/check-native-linkage +115 -0
  20. data/test/client_test.rb +30 -0
  21. data/test/extconf_link_flags_test.rb +160 -0
  22. data/vendor/c-questdb-client/.bumpversion.toml +1 -1
  23. data/vendor/c-questdb-client/CMakeLists.txt +1 -1
  24. data/vendor/c-questdb-client/cpp_test/qwp_mock_server.hpp +10 -1
  25. data/vendor/c-questdb-client/cpp_test/test_arrow_c.c +39 -0
  26. data/vendor/c-questdb-client/cpp_test/test_reader_mock.cpp +428 -0
  27. data/vendor/c-questdb-client/doc/COMPATIBILITY.md +1 -1
  28. data/vendor/c-questdb-client/doc/CONSIDERATIONS.md +6 -3
  29. data/vendor/c-questdb-client/doc/RELEASE_RUNBOOK.md +7 -7
  30. data/vendor/c-questdb-client/doc/RELEASING.md +6 -6
  31. data/vendor/c-questdb-client/include/questdb/egress/qwp_reader.h +48 -1
  32. data/vendor/c-questdb-client/include/questdb/egress/qwp_reader.hpp +52 -0
  33. data/vendor/c-questdb-client/include/questdb/ingress/line_sender.h +46 -4
  34. data/vendor/c-questdb-client/include/questdb/ingress/line_sender.hpp +7 -4
  35. data/vendor/c-questdb-client/include/questdb/ingress/line_sender_core.hpp +1 -0
  36. data/vendor/c-questdb-client/questdb-rs/Cargo.toml +1 -1
  37. data/vendor/c-questdb-client/questdb-rs/README.md +13 -13
  38. data/vendor/c-questdb-client/questdb-rs/src/egress/config.rs +78 -0
  39. data/vendor/c-questdb-client/questdb-rs/src/egress/mod.rs +2 -2
  40. data/vendor/c-questdb-client/questdb-rs/src/egress/query_request.rs +174 -1
  41. data/vendor/c-questdb-client/questdb-rs/src/egress/reader.rs +264 -14
  42. data/vendor/c-questdb-client/questdb-rs/src/egress/wire/capabilities.rs +69 -0
  43. data/vendor/c-questdb-client/questdb-rs/src/egress/wire/msg_kind.rs +33 -0
  44. data/vendor/c-questdb-client/questdb-rs/src/error.rs +33 -5
  45. data/vendor/c-questdb-client/questdb-rs/src/ingress/buffer/qwp.rs +96 -11
  46. data/vendor/c-questdb-client/questdb-rs/src/ingress/buffer.rs +6 -3
  47. data/vendor/c-questdb-client/questdb-rs/src/ingress/column_sender/sender.rs +1 -16
  48. data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws.rs +19 -13
  49. data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws_driver.rs +2 -9
  50. data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws_publisher.rs +3 -3
  51. data/vendor/c-questdb-client/questdb-rs/src/ingress/sender.rs +101 -17
  52. data/vendor/c-questdb-client/questdb-rs/src/ingress/tests.rs +32 -0
  53. data/vendor/c-questdb-client/questdb-rs/src/ingress.rs +3 -2
  54. data/vendor/c-questdb-client/questdb-rs/src/tests/qwp_ws.rs +96 -16
  55. data/vendor/c-questdb-client/questdb-rs/tests/egress_failover.rs +219 -1
  56. data/vendor/c-questdb-client/questdb-rs/tests/egress_live_server.rs +34 -0
  57. data/vendor/c-questdb-client/questdb-rs-ffi/Cargo.lock +2 -2
  58. data/vendor/c-questdb-client/questdb-rs-ffi/Cargo.toml +1 -1
  59. data/vendor/c-questdb-client/questdb-rs-ffi/src/egress.rs +31 -0
  60. data/vendor/c-questdb-client/questdb-rs-ffi/src/lib.rs +70 -3
  61. data/vendor/c-questdb-client/system_test/arrow_ffi.py +1 -0
  62. data/vendor/c-questdb-client/system_test/enterprise_e2e/tests/test_failover.py +12 -6
  63. data/vendor/c-questdb-client/system_test/enterprise_e2e/tests/test_orphan_drainer_bindings.py +12 -4
  64. data/vendor/c-questdb-client/system_test/failover_clients/Cargo.lock +1 -1
  65. data/vendor/c-questdb-client/system_test/soak/workload_rs/Cargo.lock +1 -1
  66. data/vendor/c-questdb-client.version +1 -1
  67. metadata +5 -2
@@ -1,6 +1,6 @@
1
1
  # Compatibility
2
2
 
3
- This page records the supported versions and toolchains for the 7.1.0 client
3
+ This page records the supported versions and toolchains for the 7.2.0 client
4
4
  release. It is the repository source of truth; language guides should link here
5
5
  instead of inventing their own compatibility claims.
6
6
 
@@ -93,9 +93,12 @@ Flushing every row increases overhead, while unbounded buffers increase memory
93
93
  use and recovery time.
94
94
 
95
95
  For ILP, the buffer length is the exact pending encoded byte length. For
96
- QWP/UDP it is a size hint rather than an eventual datagram size. QWP/WebSocket
97
- column and row senders encode frames at publication time and apply their own
98
- store-and-forward limits.
96
+ QWP/UDP it is a size hint rather than an eventual datagram size. For a
97
+ QWP/WebSocket row buffer it is at least the size of the frame a flush encodes,
98
+ unless store-and-forward has fallen back to full symbol dictionaries, so a
99
+ buffer whose length is within the frame limit fits in one frame.
100
+ QWP/WebSocket column and row senders encode frames at publication time and
101
+ apply their own store-and-forward limits.
99
102
 
100
103
  Pool limits (`sender_pool_max`, `query_pool_max`) bound concurrent connections.
101
104
  At capacity, borrow operations wait up to `acquire_timeout_ms` or fail
@@ -206,7 +206,7 @@ git fetch origin --tags --prune
206
206
  git switch main
207
207
  git pull --ff-only
208
208
  test -z "$(git status --porcelain=v1 --untracked-files=all)"
209
- git switch -c release/7.1.0
209
+ git switch -c release/7.2.0
210
210
  ```
211
211
 
212
212
  Do not prepare a release from a dirty feature-branch checkout.
@@ -216,7 +216,7 @@ Do not prepare a release from a dirty feature-branch checkout.
216
216
  For a future version change, preview before modifying files:
217
217
 
218
218
  ```bash
219
- NEW_VERSION=7.1.0
219
+ NEW_VERSION=7.2.0
220
220
  bump-my-version replace --new-version "$NEW_VERSION" --dry-run
221
221
  bump-my-version replace --new-version "$NEW_VERSION"
222
222
  ```
@@ -326,7 +326,7 @@ git switch main
326
326
  git pull --ff-only
327
327
  test -z "$(git status --porcelain=v1 --untracked-files=all)"
328
328
 
329
- VERSION=7.1.0
329
+ VERSION=7.2.0
330
330
  RELEASE_SHA="$(git rev-parse HEAD)"
331
331
 
332
332
  git show --no-patch --format=fuller "$RELEASE_SHA"
@@ -487,7 +487,7 @@ git switch main
487
487
  git pull --ff-only
488
488
  test -z "$(git status --porcelain=v1 --untracked-files=all)"
489
489
 
490
- VERSION=7.1.0
490
+ VERSION=7.2.0
491
491
  RELEASE_SHA="$(git rev-parse HEAD)"
492
492
 
493
493
  git show --no-patch --format=fuller "$RELEASE_SHA"
@@ -572,7 +572,7 @@ can hide packaging mistakes:
572
572
  SMOKE_ROOT="$(mktemp -d)"
573
573
  cargo new "$SMOKE_ROOT/qdb-rs-release-smoke"
574
574
  cd "$SMOKE_ROOT/qdb-rs-release-smoke"
575
- cargo add questdb-rs@=7.1.0
575
+ cargo add questdb-rs@=7.2.0
576
576
  cargo check
577
577
  ```
578
578
 
@@ -627,11 +627,11 @@ A crates.io version cannot be overwritten or deleted.
627
627
  To yank without deleting the already published package:
628
628
 
629
629
  ```bash
630
- cargo yank --version 7.1.0 questdb-rs
630
+ cargo yank --version 7.2.0 questdb-rs
631
631
  ```
632
632
 
633
633
  Yanking does not break consumers whose lockfiles already select the version.
634
- Never reuse 7.1.0 after yanking it.
634
+ Never reuse 7.2.0 after yanking it.
635
635
 
636
636
  ## Final release checklist
637
637
 
@@ -18,10 +18,10 @@ git fetch origin --tags --prune
18
18
  git switch main
19
19
  git pull --ff-only
20
20
  test -z "$(git status --porcelain=v1 --untracked-files=all)"
21
- git switch -c release/7.1.0
21
+ git switch -c release/7.2.0
22
22
  ```
23
23
 
24
- Replace `7.1.0` throughout these examples with the intended version.
24
+ Replace `7.2.0` throughout these examples with the intended version.
25
25
 
26
26
  Confirm publisher access, a crates.io token configured with `cargo login` or a
27
27
  protected environment variable, a release verifier, and a merge-ready
@@ -38,13 +38,13 @@ uv tool install bump-my-version
38
38
  Preview and then apply the version update:
39
39
 
40
40
  ```bash
41
- bump-my-version replace --new-version 7.1.0 --dry-run
42
- bump-my-version replace --new-version 7.1.0
41
+ bump-my-version replace --new-version 7.2.0 --dry-run
42
+ bump-my-version replace --new-version 7.2.0
43
43
  ```
44
44
 
45
45
  Review every changed version and update the release notes, compatibility
46
46
  statements, and this repository's documentation. The QWP/WebSocket floor for
47
- 7.1.0 is QuestDB 10.0 and the Rust MSRV is 1.91.0.
47
+ 7.2.0 is QuestDB 10.0 and the Rust MSRV is 1.91.0.
48
48
 
49
49
  Refresh every tracked lockfile containing this client:
50
50
 
@@ -85,7 +85,7 @@ Enterprise check, and release-candidate soak.
85
85
  From a clean checkout of the approved `main` commit:
86
86
 
87
87
  ```bash
88
- VERSION=7.1.0
88
+ VERSION=7.2.0
89
89
  RELEASE_SHA="$(git rev-parse HEAD)"
90
90
 
91
91
  cd questdb-rs
@@ -977,6 +977,35 @@ QUESTDB_CLIENT_API void qwp_reader_query_initial_credit(
977
977
  QUESTDB_CLIENT_API void qwp_reader_query_set_reset_symbol_dict(
978
978
  qwp_reader_query*, bool reset);
979
979
 
980
+ /**
981
+ * Run this query under its own timeout, in milliseconds, instead of the
982
+ * server-wide `query.timeout`. Overrides the connect string's
983
+ * `query_timeout_ms` for this query; `0` clears it (no client timeout).
984
+ * Mirrors `ReaderQuery::timeout`.
985
+ *
986
+ * Requires a server advertising `CAP_QUERY_TIMEOUT` (`0x08` in
987
+ * `qwp_reader_server_info_capabilities`). Unlike
988
+ * `qwp_reader_query_set_reset_symbol_dict`, this does NOT silently degrade:
989
+ * against an older server `qwp_reader_query_execute` fails with
990
+ * `line_sender_error_unsupported_server`, before anything is written, instead
991
+ * of running the query under the server default while the caller believes
992
+ * its timeout applied. The connection is untouched by that refusal. Raise
993
+ * the server's `query.timeout` instead, or clear the timeout here.
994
+ *
995
+ * The server applies no ceiling, so a value above `query.timeout` is
996
+ * honoured — which is the point for a long transform.
997
+ *
998
+ * On expiry the cursor terminates with `line_sender_error_query_timeout` and
999
+ * the connection stays open and authenticated, so the next query runs on it
1000
+ * without reconnecting. DO NOT RETRY A WRITE on that error: a statement that
1001
+ * completes past its timeout is answered with `EXEC_DONE` and its row count
1002
+ * rather than a timeout (so a retry cannot apply it twice), and a DDL /
1003
+ * INSERT / UPDATE that timed out waiting for the table writer may still be
1004
+ * applied by that writer afterwards.
1005
+ */
1006
+ QUESTDB_CLIENT_API void qwp_reader_query_set_timeout_ms(
1007
+ qwp_reader_query*, uint64_t timeout_ms);
1008
+
980
1009
  /**
981
1010
  * Install a failover-reset callback on the query. Replaces any previously
982
1011
  * installed callback. `user_data` is opaque to the library; pass NULL if
@@ -1368,13 +1397,31 @@ QUESTDB_CLIENT_API bool qwp_reader_cursor_terminal_end(
1368
1397
  /**
1369
1398
  * If the terminal is `EXEC_DONE`, fill the output parameters and return
1370
1399
  * true; otherwise zeroes both outputs and returns false.
1400
+ *
1401
+ * `*out_rows_affected` is the row count of an `INSERT` / `UPDATE`. For a
1402
+ * statement the server executes at parse time (`TRUNCATE`, `RENAME TABLE`,
1403
+ * `SET`, ...) it reports no count: it sends `-1`, which arrives as
1404
+ * `QWP_READER_ROWS_AFFECTED_UNKNOWN` (`UINT64_MAX`). `*out_op_type` is the
1405
+ * server's operation-type byte, carried through opaquely.
1371
1406
  */
1372
1407
  QUESTDB_CLIENT_API bool qwp_reader_cursor_terminal_exec_done(
1373
1408
  const qwp_reader_cursor* cursor,
1374
1409
  uint8_t* out_op_type,
1375
1410
  uint64_t* out_rows_affected);
1376
1411
 
1377
- /** Whether freeing this cursor leaves its reader reusable; false for NULL. */
1412
+ /** `*out_rows_affected` of `qwp_reader_cursor_terminal_exec_done` for a
1413
+ * statement that reports no row count. */
1414
+ #define QWP_READER_ROWS_AFFECTED_UNKNOWN UINT64_MAX
1415
+
1416
+ /**
1417
+ * Whether freeing this cursor leaves its reader reusable; false for NULL.
1418
+ *
1419
+ * True only once the stream reached a terminal frame (or was cancelled to
1420
+ * one) with no query still active and the transport up. A cursor that is
1421
+ * terminal only because a client-side error ended it mid-stream still has
1422
+ * frames in flight; freeing it sends `CANCEL` and closes the transport, so
1423
+ * this reports false for it.
1424
+ */
1378
1425
  QUESTDB_CLIENT_API bool qwp_reader_cursor_connection_reusable(
1379
1426
  const qwp_reader_cursor* cursor);
1380
1427
 
@@ -27,6 +27,8 @@
27
27
  #include "qwp_reader.h"
28
28
 
29
29
  #include <array>
30
+ #include <chrono>
31
+ #include <cmath>
30
32
  #include <cstdint>
31
33
  #include <cstring>
32
34
  #include <functional>
@@ -1195,6 +1197,56 @@ public:
1195
1197
  return *this;
1196
1198
  }
1197
1199
 
1200
+ /**
1201
+ * Run this query under its own timeout instead of the server-wide
1202
+ * `query.timeout`. Overrides the connect string's `query_timeout_ms`;
1203
+ * `0` clears it. Requires a server advertising `CAP_QUERY_TIMEOUT`
1204
+ * (`0x08`) — against an older one `execute()` throws
1205
+ * `error_code::unsupported_server`, before anything is written, rather
1206
+ * than silently running under the server default; the connection is
1207
+ * untouched. The server applies no ceiling, so a value above
1208
+ * `query.timeout` is honoured.
1209
+ *
1210
+ * On expiry the cursor terminates with `error_code::query_timeout` and
1211
+ * the connection stays usable. Do not retry a write on it: a statement
1212
+ * that outlives its timeout is reported as `EXEC_DONE` instead, and a
1213
+ * DDL / INSERT / UPDATE that timed out waiting for the table writer may
1214
+ * still be applied afterwards.
1215
+ */
1216
+ query& timeout_ms(uint64_t timeout_ms)
1217
+ {
1218
+ ensure_impl();
1219
+ ::qwp_reader_query_set_timeout_ms(_impl, timeout_ms);
1220
+ return *this;
1221
+ }
1222
+
1223
+ /** `timeout_ms` taking a `std::chrono` duration. A non-zero duration
1224
+ * below 1 ms rounds up to 1 ms, never down to "no timeout"; one too
1225
+ * large for the wire saturates at its maximum. Only zero clears the
1226
+ * timeout: a negative, NaN or infinite duration throws
1227
+ * `error_code::invalid_api_call`. */
1228
+ template <typename Rep, typename Period>
1229
+ query& timeout(std::chrono::duration<Rep, Period> d)
1230
+ {
1231
+ // Convert in double: `duration_cast` to an integral rep is UB for
1232
+ // NaN and for values outside the target range.
1233
+ const double ms = std::chrono::duration<double, std::milli>{d}.count();
1234
+ if (std::isnan(ms) || std::isinf(ms))
1235
+ throw ::questdb::error{
1236
+ error_code::invalid_api_call,
1237
+ "query::timeout(): duration must be finite"};
1238
+ if (ms < 0.0)
1239
+ throw ::questdb::error{
1240
+ error_code::invalid_api_call,
1241
+ "query::timeout(): duration must not be negative"};
1242
+ if (ms == 0.0)
1243
+ return timeout_ms(0);
1244
+ if (ms >= 18446744073709551616.0) // 2^64
1245
+ return timeout_ms(UINT64_MAX);
1246
+ const auto as_u64 = static_cast<uint64_t>(ms);
1247
+ return timeout_ms(as_u64 == 0 ? 1 : as_u64);
1248
+ }
1249
+
1198
1250
  /**
1199
1251
  * Install a failover-reset callback. Replaces any previously installed
1200
1252
  * callback. The closure is stored on the heap and remains alive for
@@ -168,7 +168,11 @@ typedef enum line_sender_error_code
168
168
  /** HTTP-upgrade or WebSocket handshake failure. */
169
169
  line_sender_error_handshake_error = 20,
170
170
 
171
- /** Server returned an unsupported QWP version, encoding, or capability. */
171
+ /** Server returned an unsupported QWP version, encoding, or capability.
172
+ * Also raised client-side, before anything is written, when a query
173
+ * carries a per-query timeout and the server does not advertise
174
+ * `CAP_QUERY_TIMEOUT` (see `qwp_reader_query_set_timeout_ms`); the
175
+ * connection is untouched by that refusal. */
172
176
  line_sender_error_unsupported_server = 21,
173
177
 
174
178
  /** Wire-format violation: bad magic, truncated frame, unknown
@@ -290,6 +294,24 @@ typedef enum line_sender_error_code
290
294
  * queued prefix already carried are duplicated.
291
295
  */
292
296
  line_sender_error_symbol_dict_full = 37,
297
+
298
+ /** The query ran past the timeout it carried: server-reported QWP
299
+ * `QUERY_TIMEOUT` (status `0x0E`), produced only for a request that
300
+ * supplied `timeout_ms` (see `qwp_reader_query_set_timeout_ms`). A
301
+ * timeout requested against a server that does not advertise
302
+ * `CAP_QUERY_TIMEOUT` is refused with
303
+ * `line_sender_error_unsupported_server` instead, so retrying a timeout
304
+ * with a larger budget cannot loop against a server that ignores it.
305
+ *
306
+ * Per-query, not per-connection: the connection stays open and
307
+ * authenticated, so the next query runs on it without reconnecting.
308
+ *
309
+ * DO NOT RETRY A WRITE ON THIS ERROR. A statement that completes past
310
+ * its timeout is reported as `EXEC_DONE` with its row count rather than
311
+ * as a timeout, precisely so a retrying client cannot apply it twice;
312
+ * and a DDL / INSERT / UPDATE that timed out waiting for the table
313
+ * writer may still be applied by that writer afterwards. */
314
+ line_sender_error_query_timeout = 38,
293
315
  } line_sender_error_code;
294
316
 
295
317
  /**
@@ -352,6 +374,7 @@ typedef line_sender_error_code questdb_error_code;
352
374
  #define questdb_error_store_resend_required \
353
375
  line_sender_error_store_resend_required
354
376
  #define questdb_error_symbol_dict_full line_sender_error_symbol_dict_full
377
+ #define questdb_error_query_timeout line_sender_error_query_timeout
355
378
 
356
379
  /** The protocol used to connect with. */
357
380
  typedef enum line_sender_protocol
@@ -833,9 +856,12 @@ void line_sender_buffer_clear(line_sender_buffer* buffer);
833
856
  /**
834
857
  * The current encoded size of the buffered data.
835
858
  *
836
- * For ILP buffers this is the exact pending byte length. For QWP buffers this
837
- * is a buffered size hint, not the exact size of an eventual UDP datagram or
838
- * WebSocket replay frame.
859
+ * For ILP buffers this is the exact pending byte length. For QWP/UDP buffers
860
+ * this is a buffered size hint, not the exact size of an eventual datagram.
861
+ * For QWP/WebSocket buffers it is at least the size of the frame that
862
+ * flushing the buffer encodes, unless store-and-forward has fallen back to
863
+ * sending full symbol dictionaries because its symbol side-file could not be
864
+ * opened or rolled back.
839
865
  */
840
866
  QUESTDB_CLIENT_API
841
867
  size_t line_sender_buffer_size(const line_sender_buffer* buffer);
@@ -2193,6 +2219,22 @@ bool line_sender_qwpws_errors_dropped(
2193
2219
  uint64_t* dropped_out,
2194
2220
  line_sender_error** err_out);
2195
2221
 
2222
+ /**
2223
+ * Return the current hard per-frame cap of a QWP/WebSocket sender: the
2224
+ * smallest of `max_buf_size`, the server's advertised `X-QWP-Max-Batch-Size`
2225
+ * and the store-and-forward segment payload capacity. A flush whose encoded
2226
+ * frame exceeds it fails with `line_sender_error_batch_too_large` and
2227
+ * publishes nothing. `*server_cap_known_out` is true when the current
2228
+ * connection advertised `X-QWP-Max-Batch-Size`, whether or not that value is
2229
+ * the binding limit. The value can change after reconnect.
2230
+ */
2231
+ QUESTDB_CLIENT_API
2232
+ bool line_sender_qwpws_effective_frame_cap(
2233
+ const line_sender* sender,
2234
+ size_t* cap_out,
2235
+ bool* server_cap_known_out,
2236
+ line_sender_error** err_out);
2237
+
2196
2238
  /**
2197
2239
  * Stop accepting new QWP/WebSocket publications and wait for already-published
2198
2240
  * frames to resolve. The timeout is configured with the QWP/WebSocket
@@ -224,9 +224,12 @@ public:
224
224
  /**
225
225
  * The current encoded size of the buffered data.
226
226
  *
227
- * For ILP buffers this is the exact pending byte length. For QWP buffers
228
- * this is a buffered size hint, not the exact size of any eventual UDP
229
- * datagram.
227
+ * For ILP buffers this is the exact pending byte length. For QWP/UDP
228
+ * buffers this is a buffered size hint, not the exact size of any
229
+ * eventual datagram. For QWP/WebSocket buffers it is at least the size of
230
+ * the frame that flushing the buffer encodes, unless store-and-forward
231
+ * has fallen back to sending full symbol dictionaries because its symbol
232
+ * side-file could not be opened or rolled back.
230
233
  */
231
234
  size_t size() const noexcept
232
235
  {
@@ -1259,7 +1262,7 @@ private:
1259
1262
  static inline ::line_sender_utf8 name()
1260
1263
  {
1261
1264
  // Maintained by .bumpversion.cfg
1262
- static const char user_agent[] = "questdb/c++/7.1.0";
1265
+ static const char user_agent[] = "questdb/c++/7.2.0";
1263
1266
  ::line_sender_utf8 utf8 =
1264
1267
  ::line_sender_utf8_assert(sizeof(user_agent) - 1, user_agent);
1265
1268
  return utf8;
@@ -104,6 +104,7 @@ enum class error_code : int
104
104
  batch_too_large = ::line_sender_error_batch_too_large,
105
105
  store_resend_required = ::line_sender_error_store_resend_required,
106
106
  symbol_dict_full = ::line_sender_error_symbol_dict_full,
107
+ query_timeout = ::line_sender_error_query_timeout,
107
108
  };
108
109
  // clang-format on
109
110
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "questdb-rs"
3
- version = "7.1.0"
3
+ version = "7.2.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.91.0"
6
6
  license = "Apache-2.0"
@@ -24,7 +24,7 @@ also supported for ingestion.
24
24
  * [Repository compatibility matrix](../doc/COMPATIBILITY.md)
25
25
  * [InfluxDB Line Protocol reference](https://questdb.com/docs/reference/api/ilp/overview/)
26
26
 
27
- Version 7.1.0 requires Rust 1.91.0. QWP over WebSocket requires QuestDB 10.0
27
+ Version 7.2.0 requires Rust 1.91.0. QWP over WebSocket requires QuestDB 10.0
28
28
  or newer.
29
29
 
30
30
  ## Transports
@@ -178,21 +178,21 @@ then see the
178
178
 
179
179
  ## Examples
180
180
 
181
- A selection of usage examples is available in the [examples directory](https://github.com/questdb/c-questdb-client/tree/7.1.0/questdb-rs/examples):
181
+ A selection of usage examples is available in the [examples directory](https://github.com/questdb/c-questdb-client/tree/7.2.0/questdb-rs/examples):
182
182
 
183
183
  | Example | Description |
184
184
  |---------|-------------|
185
- | [`basic.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/basic.rs) | Minimal TCP ingestion example; shows basic row and array ingestion. |
186
- | [`auth.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/auth.rs) | Adds authentication (user/password, token) to basic ingestion. |
187
- | [`auth_tls.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/auth_tls.rs) | Like `auth.rs`, but uses TLS for encrypted TCP connections. |
188
- | [`from_conf.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/from_conf.rs) | Configures client via connection string instead of builder pattern. |
189
- | [`from_env.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/from_env.rs) | Reads config from `QDB_CLIENT_CONF` environment variable. |
190
- | [`http.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/http.rs) | Uses HTTP transport and demonstrates array ingestion with `ndarray`. |
191
- | [`protocol_version.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/protocol_version.rs) | Shows protocol version selection and feature differences (e.g. arrays). |
192
- | [`qwp_ws_chunk_and_query.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/qwp_ws_chunk_and_query.rs) | Shares one `QuestDb` pool between concurrent columnar ingestion and query workers. |
193
- | [`qwp_ws_l1_quotes.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/qwp_ws_l1_quotes.rs) | Columnar ingestion over QWP/WebSocket via the `QuestDb` connection pool. |
194
- | [`qwp_egress_read.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/qwp_egress_read.rs) | Runs a SQL query and streams the result set over QWP/WebSocket. |
195
- | [`polars.rs`](https://github.com/questdb/c-questdb-client/blob/7.1.0/questdb-rs/examples/polars.rs) | Round trip: ingests a Polars `DataFrame` and queries it back as one. |
185
+ | [`basic.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/basic.rs) | Minimal TCP ingestion example; shows basic row and array ingestion. |
186
+ | [`auth.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/auth.rs) | Adds authentication (user/password, token) to basic ingestion. |
187
+ | [`auth_tls.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/auth_tls.rs) | Like `auth.rs`, but uses TLS for encrypted TCP connections. |
188
+ | [`from_conf.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/from_conf.rs) | Configures client via connection string instead of builder pattern. |
189
+ | [`from_env.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/from_env.rs) | Reads config from `QDB_CLIENT_CONF` environment variable. |
190
+ | [`http.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/http.rs) | Uses HTTP transport and demonstrates array ingestion with `ndarray`. |
191
+ | [`protocol_version.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/protocol_version.rs) | Shows protocol version selection and feature differences (e.g. arrays). |
192
+ | [`qwp_ws_chunk_and_query.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/qwp_ws_chunk_and_query.rs) | Shares one `QuestDb` pool between concurrent columnar ingestion and query workers. |
193
+ | [`qwp_ws_l1_quotes.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/qwp_ws_l1_quotes.rs) | Columnar ingestion over QWP/WebSocket via the `QuestDb` connection pool. |
194
+ | [`qwp_egress_read.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/qwp_egress_read.rs) | Runs a SQL query and streams the result set over QWP/WebSocket. |
195
+ | [`polars.rs`](https://github.com/questdb/c-questdb-client/blob/7.2.0/questdb-rs/examples/polars.rs) | Round trip: ingests a Polars `DataFrame` and queries it back as one. |
196
196
 
197
197
  ## Crate features
198
198
 
@@ -88,6 +88,11 @@ pub const DEFAULT_PATH: &str = "/read/v1";
88
88
  pub const HIGHEST_KNOWN_VERSION: u8 = crate::egress::wire::PROTOCOL_VERSION;
89
89
 
90
90
  /// Default WS port (matches QuestDB HTTP / ILP-HTTP convention).
91
+ /// Default `query_timeout_ms`: `0`, i.e. no client-supplied per-query
92
+ /// timeout, so each query runs under the server-wide `query.timeout`.
93
+ /// Matches the Java client's `query_timeout_ms` default.
94
+ pub const DEFAULT_QUERY_TIMEOUT_MS: u64 = 0;
95
+
91
96
  const DEFAULT_PLAIN_PORT: &str = "9000";
92
97
  const DEFAULT_TLS_PORT: &str = "9000";
93
98
 
@@ -489,6 +494,19 @@ pub struct ReaderConfig {
489
494
  /// handshake, the WS upgrade (see `auth_timeout_ms`), or the
490
495
  /// `SERVER_INFO` read (see `server_info_timeout_ms`).
491
496
  pub connect_timeout_ms: u64,
497
+ /// Default per-query timeout in milliseconds, applied to every query
498
+ /// this reader runs unless the query overrides it with
499
+ /// [`ReaderQuery::timeout`](crate::egress::ReaderQuery::timeout). `0`
500
+ /// (the default) means no client timeout: each query runs under the
501
+ /// server-wide `query.timeout`.
502
+ ///
503
+ /// A non-zero value requires a server advertising `CAP_QUERY_TIMEOUT`;
504
+ /// against an older server every query is refused before anything is
505
+ /// sent, with [`crate::ErrorCode::UnsupportedServer`], rather than
506
+ /// silently running under the server default. The server applies no
507
+ /// ceiling, so this may legitimately exceed `query.timeout` — which is
508
+ /// the point for long transforms. Connect-string key: `query_timeout_ms`.
509
+ pub query_timeout_ms: u64,
492
510
  /// Client's zone identifier — opaque case-insensitive string (e.g.
493
511
  /// `eu-west-1a`, `dc-amsterdam`). When set, the host-health tracker
494
512
  /// prefers endpoints whose server-advertised `zone_id` matches
@@ -770,6 +788,7 @@ impl ReaderConfig {
770
788
  let mut auth_timeout_ms: u64 = DEFAULT_AUTH_TIMEOUT_MS;
771
789
  let server_info_timeout_ms: u64 = DEFAULT_SERVER_INFO_TIMEOUT_MS;
772
790
  let mut connect_timeout_ms: u64 = 0;
791
+ let mut query_timeout_ms: u64 = DEFAULT_QUERY_TIMEOUT_MS;
773
792
  let mut zone: Option<String> = None;
774
793
  let mut tls_verify = TlsVerify::On;
775
794
  let mut tls_ca = default_tls_ca();
@@ -914,6 +933,9 @@ impl ReaderConfig {
914
933
  "connect_timeout" => {
915
934
  connect_timeout_ms = parse_value("connect_timeout", val)?;
916
935
  }
936
+ "query_timeout_ms" => {
937
+ query_timeout_ms = parse_value("query_timeout_ms", val)?;
938
+ }
917
939
  "zone" => {
918
940
  // Empty / whitespace-only is treated as unset
919
941
  // (zone-blind). Reject CR/LF — these are headers /
@@ -1044,6 +1066,7 @@ impl ReaderConfig {
1044
1066
  auth_timeout_ms,
1045
1067
  server_info_timeout_ms,
1046
1068
  connect_timeout_ms,
1069
+ query_timeout_ms,
1047
1070
  zone,
1048
1071
  auth,
1049
1072
  tls_verify,
@@ -1986,6 +2009,61 @@ mod tests {
1986
2009
  assert!(err.msg().contains("connect_timeout"), "msg: {}", err.msg());
1987
2010
  }
1988
2011
 
2012
+ // --- query_timeout_ms (per-query timeout default) ---
2013
+
2014
+ #[test]
2015
+ fn query_timeout_defaults_to_none() {
2016
+ let c = ReaderConfig::from_conf("ws::addr=h:1").unwrap();
2017
+ assert_eq!(
2018
+ c.query_timeout_ms, 0,
2019
+ "default must leave queries under the server-wide query.timeout"
2020
+ );
2021
+ assert_eq!(DEFAULT_QUERY_TIMEOUT_MS, 0);
2022
+ }
2023
+
2024
+ #[test]
2025
+ fn query_timeout_parses_from_connect_string() {
2026
+ let c = ReaderConfig::from_conf("ws::addr=h:1;query_timeout_ms=600000").unwrap();
2027
+ assert_eq!(c.query_timeout_ms, 600_000);
2028
+ }
2029
+
2030
+ #[test]
2031
+ fn query_timeout_zero_is_no_timeout() {
2032
+ // `0` is the documented sentinel for "send no timeout_ms". The
2033
+ // server rejects a zero on the wire, so the client must express
2034
+ // "none" by omitting the field, never by sending 0.
2035
+ let c = ReaderConfig::from_conf("ws::addr=h:1;query_timeout_ms=0").unwrap();
2036
+ assert_eq!(c.query_timeout_ms, 0);
2037
+ }
2038
+
2039
+ #[test]
2040
+ fn query_timeout_has_no_ceiling() {
2041
+ // Deliberate: the server applies no ceiling either, and a dbt-style
2042
+ // transform legitimately wants longer than `query.timeout`. A cap
2043
+ // here would silently cut such a query short.
2044
+ let c = ReaderConfig::from_conf("ws::addr=h:1;query_timeout_ms=86400000").unwrap();
2045
+ assert_eq!(c.query_timeout_ms, 86_400_000);
2046
+ }
2047
+
2048
+ #[test]
2049
+ fn query_timeout_non_numeric_rejected() {
2050
+ let err = ReaderConfig::from_conf("ws::addr=h:1;query_timeout_ms=soon").unwrap_err();
2051
+ assert_eq!(err.code(), ErrorCode::ConfigError);
2052
+ assert!(
2053
+ err.msg().contains("query_timeout_ms"),
2054
+ "msg must name the key: {}",
2055
+ err.msg()
2056
+ );
2057
+ }
2058
+
2059
+ #[test]
2060
+ fn query_timeout_negative_rejected() {
2061
+ // The field is an unsigned varint on the wire; a negative value is
2062
+ // a `STATUS_PARSE_ERROR` at the server, so reject it at parse time.
2063
+ let err = ReaderConfig::from_conf("ws::addr=h:1;query_timeout_ms=-1").unwrap_err();
2064
+ assert_eq!(err.code(), ErrorCode::ConfigError);
2065
+ }
2066
+
1989
2067
  #[test]
1990
2068
  fn failover_max_duration_defaults_to_30s() {
1991
2069
  let c = ReaderConfig::from_conf("ws::addr=h:1").unwrap();
@@ -90,8 +90,8 @@ pub use config::{
90
90
  };
91
91
  #[cfg(feature = "sync-reader-qwp-ws")]
92
92
  pub use reader::{
93
- BatchView, Cursor, FailoverPhase, FailoverProgressEvent, FailoverResetEvent, Reader,
94
- ReaderQuery, ReaderStats, Terminal,
93
+ BatchView, Cursor, FailoverPhase, FailoverProgressEvent, FailoverResetEvent,
94
+ ROWS_AFFECTED_UNKNOWN, Reader, ReaderQuery, ReaderStats, Terminal,
95
95
  };
96
96
  pub use server_event::{ServerInfo, ServerRole, UpgradeReject};
97
97
  pub use symbol_dict::{SymbolDict, SymbolEntry};