@@ -52,6 +52,14 @@ public class QwpQueryClient implements QuietCloseable {
5252
5353 public static final String DEFAULT_ENDPOINT_PATH = "/read/v1" ;
5454 public static final int DEFAULT_WS_PORT = 9000 ;
55+ /**
56+ * Hard ceiling on {@link #withMaxBatchRows}. Matches the client decoder's
57+ * own {@code MAX_ROWS_PER_BATCH} safety cap so a user cannot ask for a
58+ * per-batch row count the decoder would itself refuse. The server enforces
59+ * its own (typically smaller) cap independently; this is just the client's
60+ * sanity bound.
61+ */
62+ public static final int MAX_BATCH_ROWS_UPPER_BOUND = 1_048_576 ;
5563 public static final int QWP_MAX_VERSION = 1 ;
5664 private static final int DEFAULT_IO_BUFFER_POOL_SIZE = 4 ;
5765 private static final Logger LOG = LoggerFactory .getLogger (QwpQueryClient .class );
@@ -81,6 +89,13 @@ public class QwpQueryClient implements QuietCloseable {
8189 // payload before it parks, and the client auto-replenishes by the size of
8290 // each batch as the user releases it.
8391 private long initialCreditBytes ;
92+ // Client preference for server-side per-batch row cap. 0 means "unset",
93+ // server uses its default. Set via {@code max_batch_rows=N} in the
94+ // connection string or {@link #withMaxBatchRows}. Smaller values give
95+ // streaming consumers earlier access to the first rows at the cost of
96+ // more per-batch overhead; larger values amortise fixed costs over more
97+ // rows. Server may clamp down to its own hard cap.
98+ private int maxBatchRows ;
8499 // Volatile so a cancel() call from a thread other than the one that ran
85100 // connect() sees the published reference (and a concurrent null-out from
86101 // close() is observed without a stale-reference race). The thread-safety
@@ -160,6 +175,7 @@ public static QwpQueryClient fromConfig(CharSequence configurationString) {
160175 // zstd opt-in.
161176 String compression = "raw" ;
162177 int compressionLevel = 3 ;
178+ int maxBatchRows = 0 ; // 0 = omit header, server uses its default
163179
164180 while (ConfStringParser .hasNext (configurationString , pos )) {
165181 pos = ConfStringParser .nextKey (configurationString , pos , sink );
@@ -223,6 +239,17 @@ public static QwpQueryClient fromConfig(CharSequence configurationString) {
223239 throw new IllegalArgumentException ("compression_level must be in [1, 22]" );
224240 }
225241 break ;
242+ case "max_batch_rows" :
243+ try {
244+ maxBatchRows = Integer .parseInt (value );
245+ } catch (NumberFormatException e ) {
246+ throw new IllegalArgumentException ("invalid max_batch_rows: " + value );
247+ }
248+ if (maxBatchRows < 1 || maxBatchRows > MAX_BATCH_ROWS_UPPER_BOUND ) {
249+ throw new IllegalArgumentException (
250+ "max_batch_rows must be in [1, " + MAX_BATCH_ROWS_UPPER_BOUND + "]" );
251+ }
252+ break ;
226253 default :
227254 throw new IllegalArgumentException ("unknown configuration key: " + key );
228255 }
@@ -236,6 +263,7 @@ public static QwpQueryClient fromConfig(CharSequence configurationString) {
236263 .withCompression (compression , compressionLevel );
237264 if (auth != null ) client .withAuthorization (auth );
238265 if (cid != null ) client .withClientId (cid );
266+ if (maxBatchRows > 0 ) client .withMaxBatchRows (maxBatchRows );
239267 return client ;
240268 }
241269
@@ -331,6 +359,7 @@ public void connect() {
331359 webSocketClient .setQwpMaxVersion (QWP_MAX_VERSION );
332360 webSocketClient .setQwpClientId (clientId != null ? clientId : defaultClientId ());
333361 webSocketClient .setQwpAcceptEncoding (buildAcceptEncodingHeader ());
362+ webSocketClient .setQwpMaxBatchRows (maxBatchRows );
334363 webSocketClient .connect (host , port );
335364 webSocketClient .upgrade (endpointPath , authorizationHeader );
336365 negotiatedQwpVersion = webSocketClient .getServerQwpVersion ();
@@ -505,6 +534,26 @@ public QwpQueryClient withInitialCredit(long bytes) {
505534 return this ;
506535 }
507536
537+ /**
538+ * Asks the server to cap each {@code RESULT_BATCH} at {@code rows} rows.
539+ * Useful for latency-sensitive streaming consumers that want to start
540+ * processing the first row as soon as possible -- a smaller cap flushes
541+ * the first batch sooner, at the cost of more per-batch overhead (WS
542+ * header, send syscall, schema-reference decode). The server clamps down
543+ * to its own hard limit; a value of {@code 0} (default) omits the header
544+ * and the server uses its own cap.
545+ * <p>
546+ * Must be called before {@link #connect}.
547+ */
548+ public QwpQueryClient withMaxBatchRows (int rows ) {
549+ if (rows < 1 || rows > MAX_BATCH_ROWS_UPPER_BOUND ) {
550+ throw new IllegalArgumentException (
551+ "max_batch_rows must be in [1, " + MAX_BATCH_ROWS_UPPER_BOUND + "]" );
552+ }
553+ this .maxBatchRows = rows ;
554+ return this ;
555+ }
556+
508557 private static String defaultClientId () {
509558 return "questdb-java-egress/1.0.0" ;
510559 }
0 commit comments