Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.clickhouse.client.api.data_formats;

import com.clickhouse.client.api.ClientException;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.data_formats.internal.NumberConverter;
import com.clickhouse.client.api.internal.SchemaUtils;
import com.clickhouse.client.api.metadata.TableSchema;
Expand Down Expand Up @@ -115,6 +116,14 @@ private void readNextRow() {
try {
nextRow = parser.nextRow();
hasNext = nextRow != null;
if (isServerExceptionRow(nextRow)) {
// With http_write_exception_in_output_format (default) the server reports a
// failure after the response started as one more row: {"exception": "Code: ..."}.
String message = (String) nextRow.get("exception");
nextRow = null;
hasNext = true;
pendingException = new ServerException(parseServerCode(message), message, 200, "");
}
} catch (Exception e) {
nextRow = null;
// Keep hasNext true so a hasNext()/next() loop calls next() again and
Expand All @@ -124,6 +133,33 @@ private void readNextRow() {
}
}

private static int parseServerCode(String message) {
// message starts with "Code: " and is followed by the numeric server error code and a dot.
int end = message.indexOf('.', 6);
if (end > 6) {
try {
return Integer.parseInt(message.substring(6, end).trim());
} catch (NumberFormatException ignored) {
// fall through
}
}
return ServerException.CODE_UNKNOWN;
}

private boolean isServerExceptionRow(Map<String, Object> row) {
// JSONEachRow writes every selected column in every row, so a data row has one key per
// schema column. A one-key error row can only be data when the schema is exactly
// [exception], i.e. the query selects only its own "exception" column.
if (row == null || row.size() != 1) {
return false;
}
if (schema.getColumns().size() == 1 && "exception".equals(schema.getColumns().get(0).getColumnName())) {
return false;
}
Object value = row.get("exception");
return value instanceof String && ((String) value).startsWith("Code: ");
}

@Override
public String getString(String colName) {
Object val = currentRow.get(colName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,66 @@ public void close() { }
}
}

@Test
public void testInBandServerExceptionRowIsThrown() throws Exception {
String msg = "Code: 395. DB::Exception: Value passed to 'throwIf' function is non-zero. (FUNCTION_THROW_IF_VALUE_IS_NON_ZERO)";
try (JSONEachRowFormatReader reader = readerOf(row("id", 1), row("id", 2), row("exception", msg))) {
Assert.assertNotNull(reader.next());
Assert.assertNotNull(reader.next());
Assert.assertTrue(reader.hasNext(), "the error must surface through next()");
try {
reader.next();
Assert.fail("Expected the server exception row to be thrown");
} catch (RuntimeException e) {
Assert.assertTrue(e instanceof com.clickhouse.client.api.ServerException, "Unexpected type: " + e);
Assert.assertEquals(((com.clickhouse.client.api.ServerException) e).getCode(), 395);
Assert.assertTrue(e.getMessage().contains("Code: 395"), "Unexpected message: " + e.getMessage());
}
Assert.assertFalse(reader.hasNext());
}
}

@Test
public void testInBandServerExceptionRowWithExceptionColumnInSchemaIsThrown() throws Exception {
String msg = "Code: 395. DB::Exception: Value passed to 'throwIf' function is non-zero. (FUNCTION_THROW_IF_VALUE_IS_NON_ZERO)";
try (JSONEachRowFormatReader reader = readerOf(row("query_id", "a", "exception", ""), row("exception", msg))) {
Assert.assertNotNull(reader.next());
Assert.assertTrue(reader.hasNext());
try {
reader.next();
Assert.fail("Expected the server exception row to be thrown");
} catch (RuntimeException e) {
Assert.assertTrue(e instanceof com.clickhouse.client.api.ServerException, "Unexpected type: " + e);
Assert.assertEquals(((com.clickhouse.client.api.ServerException) e).getCode(), 395);
}
}
}

@Test
public void testExceptionColumnWithOtherDataIsNotAnError() throws Exception {
try (JSONEachRowFormatReader reader = readerOf(row("exception", "Code: 1 is a value"), row("exception", "ok"))) {
Assert.assertNotNull(reader.next());
Assert.assertNotNull(reader.next());
Assert.assertFalse(reader.hasNext());
}
try (JSONEachRowFormatReader reader = readerOf(row("id", 1, "exception", "Code: 1"))) {
Assert.assertNotNull(reader.next());
Assert.assertFalse(reader.hasNext());
}
}

@Test
public void testSingleExceptionColumnWithErrorLikeValuesIsData() throws Exception {
try (JSONEachRowFormatReader reader = readerOf(
row("exception", "x"),
row("exception", "Code: 60. DB::Exception: Table doesn't exist. (UNKNOWN_TABLE)"))) {
Assert.assertNotNull(reader.next());
Assert.assertTrue(reader.hasNext());
Assert.assertNotNull(reader.next());
Assert.assertFalse(reader.hasNext());
}
}

// ---------------------------------------------------------------------
// Row navigation, readValue, hasValue
// ---------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Compatibility-sensitive traits:
- Certificate-as-content support is compatibility-sensitive: any certificate or key value containing a PEM begin marker (`-----BEGIN`) is treated as inline PEM content, otherwise it is treated as a file path (also searched in the home directory and on the classpath).
- JSONEachRow reading depends on the selected parser factory and request format settings: parser materialization determines Java value types, the reader infers minimal schema from the first row, and JSON number server settings are applied only when `QuerySettings` resolves to `ClickHouseFormat.JSONEachRow` and `json_disable_number_quoting` is enabled.
- JSONEachRow schema inference is intentionally best-effort: scalar values use Java-to-ClickHouse type mappings, while JSON arrays and objects are identified structurally as `Array` and `Map`. For arrays, maps, and some nested or ambiguous values, the inferred type may not include the most specific element, key, value, or nested ClickHouse type.
- JSONEachRow reading reports a failure that the server writes into the response body: with `http_write_exception_in_output_format` (default on) a query that fails after the response started ends with an extra row `{"exception": "Code: ..."}`. The reader throws a `ServerException` (with the parsed server error code) from `next()` for that row instead of returning it as data; rows read before it are still returned. A row is treated this way only when `exception` is its only key and the schema has other columns.
- Binary string support is scope-sensitive and off by default: it only changes how top-level `String`/`FixedString` columns are read (to `StringValue` instead of `String`); nested string values are always read as `String`. `StringValue` is produced only by the read path and has no public constructor (it cannot be instantiated by user code). It is intentionally a mutable, zero-copy holder — it wraps (does not copy) the bytes it is given and `toByteArray()` returns the live backing array when the value spans the whole array (otherwise an exact-size copy), so callers needing an independent snapshot must copy the bytes themselves.


Expand Down
2 changes: 2 additions & 0 deletions history/latest/3220.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- **[client-v2]** `JSONEachRowFormatReader` now throws a `ServerException` when the server reports a failure after the response started. With `http_write_exception_in_output_format` (default on) the server appends a row `{"exception": "Code: ..."}` and ends the response normally; the reader returned it as an ordinary all-null data row, so the failure was silent. The rows read before the error are still returned, and the exception surfaces on the next `next()` call with the server error code. A query that selects only an `exception` column is not affected.
(https://github.com/ClickHouse/clickhouse-java/issues/3220)