Skip to content

Commit c7012df

Browse files
kafka1991claude
andcommitted
docs(examples): make the pooled QuestDB facade the first-class API
Convert the QWP query examples from the low-level QwpQueryClient to the pooled QuestDB facade (db.query()/db.executeSql()), and add ws-pooled ingest and facade examples so the recommended entry point is what the examples lead with. Keep one low-level QwpQueryClient example and the ILP Sender examples as the legacy-transport reference. New: - root: QuestDBExample (ingest+query round-trip), QuestDBSeparateConfigExample, ConnectionErrorExample, BinaryColumnExample - sender (ws): WsExample, WsConcurrentIngestExample, WsColumnTypesExample, WsArrayExample, WsAuthTlsExample, WsAdvancedExample, WsStoreAndForwardExample, WsFailoverExample, SenderErrorHandlingExample (low-level) - query: QwpQueryClientExample (low-level), ArrayResultExample, MultiInFlightQueryExample, QueryCancellationExample, QueryFailoverExample Wire qwp-pool/qwp-ingest/qwp-query into examples.manifest.yaml (mirroring the Go client) while keeping the ilp* entries. Verified by running every example against a live QuestDB; fixes from that: - BindAllTypesExample: ::GEOHASH(60b) is invalid (cast(.. as GEOHASH) works); ::FLOAT is widened to DOUBLE, so read c_float as a double - WsArrayExample: only double arrays are supported for ingest - ErrorHandlingExample: COPY isn't rejected on egress; use an invalid column - ConnectionErrorExample: connect() eagerly warms the sender pool, so bad hosts surface as LineSenderException Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 660a06b commit c7012df

40 files changed

Lines changed: 2130 additions & 837 deletions

examples.manifest.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
- name: qwp-pool
2+
lang: java
3+
path: examples/src/main/java/com/example/QuestDBExample.java
4+
header: |-
5+
QuestDB Java client [docs](https://questdb.io/docs/connect/clients/java/)
6+
and [Maven artifact](https://central.sonatype.com/artifact/org.questdb/questdb-client).
7+
- name: qwp-ingest
8+
lang: java
9+
path: examples/src/main/java/com/example/sender/WsExample.java
10+
header: |-
11+
QuestDB Java client [docs](https://questdb.io/docs/connect/clients/java/)
12+
and [Maven artifact](https://central.sonatype.com/artifact/org.questdb/questdb-client).
13+
- name: qwp-query
14+
lang: java
15+
path: examples/src/main/java/com/example/query/BasicQueryExample.java
16+
header: |-
17+
QuestDB Java client [docs](https://questdb.io/docs/connect/clients/java/)
18+
and [Maven artifact](https://central.sonatype.com/artifact/org.questdb/questdb-client).
119
- name: ilp-http
220
lang: java
321
path: examples/src/main/java/com/example/sender/HttpExample.java
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.example;
2+
3+
import io.questdb.client.QueryException;
4+
import io.questdb.client.QuestDB;
5+
import io.questdb.client.Sender;
6+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
7+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
8+
9+
import java.util.Arrays;
10+
11+
/**
12+
* BINARY column round-trip: ingest a {@code byte[]} and read it back.
13+
* <p>
14+
* Ingest with {@code binaryColumn(name, byte[])} (overloads also take a native
15+
* {@code (ptr, len)} pair or a {@code DirectByteSlice} for zero-copy paths).
16+
* On egress, {@link QwpColumnBatch#getBinary(int, int)} returns a heap
17+
* {@code byte[]}; {@code getBinaryA}/{@code getBinaryB} give reusable native
18+
* views for allocation-free reads.
19+
*/
20+
public class BinaryColumnExample {
21+
22+
public static void main(String[] args) throws InterruptedException {
23+
byte[] payload = {0x00, 0x01, 0x02, (byte) 0xFF};
24+
25+
try (QuestDB db = QuestDB.connect("ws::addr=localhost:9000;")) {
26+
27+
try (Sender sender = db.borrowSender()) {
28+
sender.table("blobs")
29+
.symbol("kind", "thumbnail")
30+
.binaryColumn("payload", payload)
31+
.atNow();
32+
}
33+
34+
try {
35+
db.executeSql(
36+
"SELECT kind, payload FROM blobs LIMIT 10",
37+
new QwpColumnBatchHandler() {
38+
@Override
39+
public void onBatch(QwpColumnBatch batch) {
40+
for (int row = 0; row < batch.getRowCount(); row++) {
41+
String kind = batch.getSymbol(0, row);
42+
byte[] bytes = batch.isNull(1, row) ? null : batch.getBinary(1, row);
43+
System.out.println("kind=" + kind + " payload=" + Arrays.toString(bytes));
44+
}
45+
}
46+
47+
@Override
48+
public void onEnd(long totalRows) {
49+
System.out.println("done: " + totalRows + " rows");
50+
}
51+
52+
@Override
53+
public void onError(byte status, String message) {
54+
System.err.printf("error: 0x%02X %s%n", status & 0xFF, message);
55+
}
56+
}
57+
).await();
58+
} catch (QueryException e) {
59+
System.err.printf("query failed: status=0x%02X %s%n", e.getStatus() & 0xFF, e.getMessage());
60+
}
61+
}
62+
}
63+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.example;
2+
3+
import io.questdb.client.QueryException;
4+
import io.questdb.client.QuestDB;
5+
import io.questdb.client.Sender;
6+
import io.questdb.client.cutlass.http.client.HttpClientException;
7+
import io.questdb.client.cutlass.line.LineSenderException;
8+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
9+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
10+
import io.questdb.client.cutlass.qwp.client.QwpRoleMismatchException;
11+
12+
/**
13+
* Connection-level failures on the pooled facade.
14+
* <p>
15+
* Some failures happen when the pool opens a WebSocket, before any row or query
16+
* is exchanged. The pool warms up its minimum connections at {@code connect()}
17+
* and opens more on demand, so a connect-level error can surface either from
18+
* {@link QuestDB#connect} or from the first {@link QuestDB#borrowSender()} /
19+
* query -- wrap both.
20+
* <ul>
21+
* <li><b>Authentication failure</b> (401/403 on the upgrade) is terminal
22+
* across all endpoints and surfaces as {@link LineSenderException} on the
23+
* ingest side.</li>
24+
* <li><b>Role mismatch</b>: when every listed endpoint reports a role that
25+
* doesn't match the query-side {@code target=} filter, the client raises
26+
* {@link QwpRoleMismatchException}.</li>
27+
* <li><b>Unreachable / unresolvable host</b>: a generic connect failure
28+
* surfaces as {@link LineSenderException} on the ingest side and as
29+
* {@link HttpClientException} (the {@code QwpRoleMismatchException}
30+
* supertype) on the query side.</li>
31+
* </ul>
32+
*/
33+
public class ConnectionErrorExample {
34+
35+
public static void main(String[] args) throws InterruptedException {
36+
37+
// 1. Bad credentials -> auth failure on the ingest connection.
38+
try (QuestDB db = QuestDB.connect(
39+
"wss::addr=db.example.com:9000;token=INVALID_TOKEN;")) {
40+
try (Sender sender = db.borrowSender()) {
41+
sender.table("trades").doubleColumn("price", 1.0).atNow();
42+
}
43+
} catch (LineSenderException | HttpClientException e) {
44+
System.err.println("ingest connection failed (auth/unreachable?): " + e.getMessage());
45+
}
46+
47+
// 2. target=replica but no listed endpoint is a replica -> role mismatch.
48+
try (QuestDB db = QuestDB.connect(
49+
"wss::addr=db-primary:9000;token=YOUR_TOKEN;target=replica;")) {
50+
try {
51+
db.executeSql("SELECT 1", new QwpColumnBatchHandler() {
52+
@Override
53+
public void onBatch(QwpColumnBatch batch) {
54+
}
55+
56+
@Override
57+
public void onEnd(long totalRows) {
58+
}
59+
60+
@Override
61+
public void onError(byte status, String message) {
62+
System.err.printf("error: 0x%02X %s%n", status & 0xFF, message);
63+
}
64+
}).await();
65+
} catch (QueryException e) {
66+
System.err.printf("query failed: status=0x%02X %s%n", e.getStatus() & 0xFF, e.getMessage());
67+
}
68+
} catch (QwpRoleMismatchException e) {
69+
System.err.println("no endpoint matched target=replica: " + e.getMessage());
70+
} catch (LineSenderException | HttpClientException e) {
71+
System.err.println("connection failed: " + e.getMessage());
72+
}
73+
}
74+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.example;
2+
3+
import io.questdb.client.Completion;
4+
import io.questdb.client.QueryException;
5+
import io.questdb.client.QuestDB;
6+
import io.questdb.client.Sender;
7+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
8+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
9+
10+
/**
11+
* The recommended entry point: the pooled {@link QuestDB} handle.
12+
* <p>
13+
* {@code QuestDB.connect(...)} owns connection pools for <b>both</b> directions
14+
* over QWP (the QuestDB WebSocket protocol): a {@link Sender} pool for ingest
15+
* and a query-client pool for egress. One {@code ws}/{@code wss} connect string
16+
* configures both -- each side reads the keys it owns and ignores the rest.
17+
* <p>
18+
* Construct one {@code QuestDB} per deployment, share it across threads, and
19+
* close it at shutdown. This example does the whole round trip on one thread:
20+
* borrow a {@link Sender} to ingest a couple of rows, then run a {@code SELECT}
21+
* to read them back.
22+
* <p>
23+
* For ingest-only and query-only variants, see
24+
* {@code com.example.sender.WsExample} and
25+
* {@code com.example.query.BasicQueryExample}.
26+
*/
27+
public class QuestDBExample {
28+
29+
public static void main(String[] args) throws InterruptedException {
30+
// One handle for the whole deployment. close() (try-with-resources)
31+
// shuts the pools down and disconnects every underlying client.
32+
try (QuestDB db = QuestDB.connect("ws::addr=localhost:9000;")) {
33+
34+
// Ingest: borrow a Sender, write rows, close() flushes it and
35+
// returns it to the pool. The WebSocket is NOT torn down here --
36+
// a real disconnect only happens at db.close().
37+
try (Sender sender = db.borrowSender()) {
38+
sender.table("trades")
39+
.symbol("symbol", "ETH-USD")
40+
.symbol("side", "sell")
41+
.doubleColumn("price", 2615.54)
42+
.doubleColumn("amount", 0.00044)
43+
.atNow();
44+
sender.table("trades")
45+
.symbol("symbol", "BTC-USD")
46+
.symbol("side", "sell")
47+
.doubleColumn("price", 39269.98)
48+
.doubleColumn("amount", 0.001)
49+
.atNow();
50+
}
51+
52+
// Query: one-shot SELECT. executeSql(...) borrows a query client
53+
// from the egress pool, runs the SQL, and returns a Completion.
54+
// (Freshly ingested rows land in the WAL asynchronously, so an
55+
// immediate read-back may not see them yet -- that is expected.)
56+
Completion c = db.executeSql(
57+
"SELECT symbol, side, price, amount FROM trades "
58+
+ "WHERE symbol = 'ETH-USD' LIMIT 10",
59+
new PrintingHandler());
60+
try {
61+
c.await(); // blocks until onEnd / onError has run
62+
} catch (QueryException e) {
63+
// Server reported an error (parse failure, schema mismatch, ...).
64+
System.err.printf("query failed: status=0x%02X %s%n",
65+
e.getStatus() & 0xFF, e.getMessage());
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Prints each row. The {@code QwpColumnBatch} is valid only during the
72+
* {@code onBatch} callback -- copy values out if you need them later.
73+
*/
74+
private static final class PrintingHandler implements QwpColumnBatchHandler {
75+
@Override
76+
public void onBatch(QwpColumnBatch batch) {
77+
batch.forEachRow(row -> System.out.printf(
78+
"symbol=%s side=%s price=%.4f amount=%.5f%n",
79+
row.getSymbol(0),
80+
row.getSymbol(1),
81+
row.getDoubleValue(2),
82+
row.getDoubleValue(3)));
83+
}
84+
85+
@Override
86+
public void onEnd(long totalRows) {
87+
System.out.println("done: " + totalRows + " rows");
88+
}
89+
90+
@Override
91+
public void onError(byte status, String message) {
92+
System.err.printf("error: 0x%02X %s%n", status & 0xFF, message);
93+
}
94+
}
95+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.example;
2+
3+
import io.questdb.client.QueryException;
4+
import io.questdb.client.QuestDB;
5+
import io.questdb.client.Sender;
6+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
7+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
8+
9+
/**
10+
* Pointing ingest and query at different endpoints.
11+
* <p>
12+
* The single-string {@link QuestDB#connect(CharSequence)} configures both
13+
* directions from one {@code ws}/{@code wss} string -- the common case. When
14+
* ingest and egress differ (ingest goes to the primary while reads target a
15+
* read-replica or a separate load balancer), pass two strings to
16+
* {@link QuestDB#connect(CharSequence, CharSequence)}, or use the builder's
17+
* {@code ingestConfig(...)} / {@code queryConfig(...)}. Each string still
18+
* accepts keys owned by the other direction; the split just lets the two sides
19+
* point at different hosts or carry different tuning.
20+
*/
21+
public class QuestDBSeparateConfigExample {
22+
23+
public static void main(String[] args) throws InterruptedException {
24+
// First string is the ingest (Sender) config, second is the query
25+
// (egress) config. Both must use the ws/wss schema.
26+
try (QuestDB db = QuestDB.connect(
27+
"ws::addr=ingest.cluster:9000;",
28+
"wss::addr=read-replica.cluster:9000;token=YOUR_TOKEN;")) {
29+
30+
try (Sender sender = db.borrowSender()) {
31+
sender.table("trades")
32+
.symbol("symbol", "ETH-USD")
33+
.doubleColumn("price", 2615.54)
34+
.atNow();
35+
}
36+
37+
try {
38+
db.executeSql("SELECT count() FROM trades", new QwpColumnBatchHandler() {
39+
@Override
40+
public void onBatch(QwpColumnBatch batch) {
41+
batch.forEachRow(row -> System.out.println("count = " + row.getLongValue(0)));
42+
}
43+
44+
@Override
45+
public void onEnd(long totalRows) {
46+
}
47+
48+
@Override
49+
public void onError(byte status, String message) {
50+
System.err.printf("error: 0x%02X %s%n", status & 0xFF, message);
51+
}
52+
}).await();
53+
} catch (QueryException e) {
54+
System.err.printf("query failed: status=0x%02X %s%n", e.getStatus() & 0xFF, e.getMessage());
55+
}
56+
}
57+
58+
// The connect string can also come from the QDB_CLIENT_CONF environment
59+
// variable (export QDB_CLIENT_CONF="wss::addr=db:9000;token=...;"):
60+
//
61+
// String cfg = System.getenv("QDB_CLIENT_CONF");
62+
// try (QuestDB db = QuestDB.connect(cfg)) { ... }
63+
}
64+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.example.query;
2+
3+
import io.questdb.client.QueryException;
4+
import io.questdb.client.QuestDB;
5+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
6+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
7+
8+
import java.util.Arrays;
9+
10+
/**
11+
* Reading array columns from a result set.
12+
* <p>
13+
* {@link QwpColumnBatch#getDoubleArrayElements(int, int)} returns a flattened
14+
* {@code double[]} in row-major order; {@link QwpColumnBatch#getArrayNDims(int, int)}
15+
* reports the dimensionality so you can interpret that flat layout. This is the
16+
* egress counterpart to {@link com.example.sender.WsArrayExample} (ingest).
17+
* <p>
18+
* Alternatively, extract individual elements in SQL (e.g. {@code SELECT
19+
* bids[1][1] FROM market_data}) and read them as scalar doubles.
20+
* <p>
21+
* Assumes a table with a {@code DOUBLE[]} column, e.g. the one
22+
* {@code WsArrayExample} writes:
23+
* <pre>
24+
* CREATE TABLE book (levels DOUBLE[], ts TIMESTAMP)
25+
* TIMESTAMP(ts) PARTITION BY DAY WAL;
26+
* </pre>
27+
*/
28+
public class ArrayResultExample {
29+
30+
public static void main(String[] args) throws InterruptedException {
31+
try (QuestDB db = QuestDB.connect("ws::addr=localhost:9000;")) {
32+
try {
33+
db.executeSql(
34+
"SELECT levels FROM book LIMIT 10",
35+
new QwpColumnBatchHandler() {
36+
@Override
37+
public void onBatch(QwpColumnBatch batch) {
38+
for (int row = 0; row < batch.getRowCount(); row++) {
39+
if (batch.isNull(0, row)) {
40+
System.out.println("levels: null");
41+
continue;
42+
}
43+
int nDims = batch.getArrayNDims(0, row);
44+
double[] flat = batch.getDoubleArrayElements(0, row);
45+
System.out.printf("levels: nDims=%d values=%s%n",
46+
nDims, Arrays.toString(flat));
47+
}
48+
}
49+
50+
@Override
51+
public void onEnd(long totalRows) {
52+
System.out.println("done: " + totalRows + " rows");
53+
}
54+
55+
@Override
56+
public void onError(byte status, String message) {
57+
System.err.printf("error: 0x%02X %s%n", status & 0xFF, message);
58+
}
59+
}
60+
).await();
61+
} catch (QueryException e) {
62+
System.err.printf("query failed: status=0x%02X %s%n", e.getStatus() & 0xFF, e.getMessage());
63+
}
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)