Skip to content

Commit 88e5046

Browse files
rustyconoverclaude
andcommitted
The SDK's own errors carry gRPC-style codes, so a bad input reads as INVALID_ARGUMENT, not a worker bug
Four exception types in farm.query.vgi implement vgirpc's HasErrorCode (WIRE_PROTOCOL §8) and subclass the JDK type each site threw before, so existing catch clauses keep working. No error_kind is set. - InvalidArgumentException (IllegalArgumentException, INVALID_ARGUMENT): every ParameterExtractor constraint, ScalarFn's type-bound check, an unparseable AT version/timestamp or unknown at_unit, an unsatisfiable implementation_version / data_version_spec. - NotFoundException (IllegalArgumentException, NOT_FOUND): unknown function / aggregate / buffering function / scalar, a function not registered in the requested catalog or schema, unknown table in scan_function_get / scan_branches_get, InMemoryCatalog's missing schema / table / view, an AT version the table does not have. - ReadOnlyCatalogException (UnsupportedOperationException, FAILED_PRECONDITION): DDL against a catalog that is not writable, from VgiServiceImpl.requireHosted and CatalogInterface's default refusals. - UnimplementedException (UnsupportedOperationException, UNIMPLEMENTED): ADD/DROP COLUMN, which no catalog this SDK hosts supports. The example worker's double() type rejection throws InvalidArgumentException too, so both cross-SDK cases in unary_error_propagation.test (double('abc') and sequence(10, batch_size := 0)) now answer INVALID_ARGUMENT. Messages are unchanged. JavaWorkerHttpConformanceTest asserts the decoded error_code end to end through the vgi-rpc HTTP client for those two cases, an unknown function (NOT_FOUND) and DDL on the read-only example catalog (FAILED_PRECONDITION); SdkErrorCodesTest pins each type's code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 95706d5 commit 88e5046

15 files changed

Lines changed: 350 additions & 51 deletions

File tree

‎vgi-example-worker/src/main/java/farm/query/vgi/example/scalar/DoubleFunction.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
package farm.query.vgi.example.scalar;
44

5+
import farm.query.vgi.InvalidArgumentException;
56
import farm.query.vgi.function.Arguments;
67
import farm.query.vgi.function.TypeBoundPredicate;
78
import farm.query.vgi.scalar.ScalarFn;
@@ -46,7 +47,7 @@ protected ArrowType outputType(Schema inputSchema, Arguments arguments) {
4647
|| in instanceof ArrowType.LargeUtf8
4748
|| in instanceof ArrowType.Binary
4849
|| in instanceof ArrowType.LargeBinary) {
49-
throw new IllegalArgumentException(
50+
throw new InvalidArgumentException(
5051
"double: _is_multipliable_type rejects " + in
5152
+ " — only numeric types (int/float/decimal) are supported");
5253
}

‎vgi-example-worker/src/test/java/farm/query/vgi/example/client/AbstractVgiHttpConformanceTest.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ private static CatalogAttachRequest attachRequest() {
598598
return CatalogAttachRequest.of(CATALOG, new byte[0], "", "");
599599
}
600600

601-
private BindRequest tableBind(String function, byte[] arguments) {
601+
protected BindRequest tableBind(String function, byte[] arguments) {
602602
return new BindRequest(
603603
function, arguments, "TABLE",
604604
null, // input_schema — producer mode

‎vgi-example-worker/src/test/java/farm/query/vgi/example/client/JavaWorkerHttpConformanceTest.java‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,24 @@
22

33
package farm.query.vgi.example.client;
44

5+
import farm.query.vgi.client.ArgumentsEncoder;
6+
import farm.query.vgi.client.SettingsEncoder;
7+
import farm.query.vgi.internal.SchemaUtil;
8+
import farm.query.vgi.protocol.BindRequest;
9+
import farm.query.vgirpc.RpcError;
10+
import farm.query.vgirpc.errors.Code;
11+
import org.apache.arrow.vector.types.pojo.ArrowType;
12+
import org.apache.arrow.vector.types.pojo.Field;
13+
import org.apache.arrow.vector.types.pojo.Schema;
514
import org.junit.jupiter.api.AfterAll;
615
import org.junit.jupiter.api.BeforeAll;
16+
import org.junit.jupiter.api.Test;
17+
18+
import java.util.List;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
723

824
/**
925
* The Java leg of {@link AbstractVgiHttpConformanceTest}: the shared assertions
@@ -12,6 +28,11 @@
1228
* <p>Always runs — it needs nothing beyond this build — so a green Python leg
1329
* and a green Java leg together are what prove agreement, and a red one alone
1430
* names the side that is wrong.
31+
*
32+
* <p>It also pins the gRPC-style {@code error_code} the SDK puts on its own
33+
* errors (vgi-rpc WIRE_PROTOCOL §8). That half is Java-only for now: the
34+
* assertions are decoded from the wire by the vgi-rpc client, exactly as the
35+
* DuckDB extension reads them.
1536
*/
1637
final class JavaWorkerHttpConformanceTest extends AbstractVgiHttpConformanceTest {
1738

@@ -31,4 +52,42 @@ static void stopWorker() {
3152
protected VgiHttpWorkerUnderTest worker() {
3253
return worker;
3354
}
55+
56+
/** {@code SELECT example.main.double('abc')}: a scalar's type rejection is the caller's fault. */
57+
@Test
58+
void scalarTypeRejectionIsInvalidArgument() {
59+
Schema input = new Schema(List.of(Field.nullable("value", new ArrowType.Utf8())));
60+
BindRequest bind = new BindRequest(
61+
"double", ArgumentsEncoder.builder().encode(), "SCALAR",
62+
SchemaUtil.serializeSchema(input), SettingsEncoder.builder().encode(), null,
63+
handle, null, false, null, null, null, null, "main");
64+
RpcError e = assertThrows(RpcError.class, () -> vgi.bind(bind, null));
65+
assertEquals(Code.INVALID_ARGUMENT.name(), e.errorCode(), e.errorMessage());
66+
}
67+
68+
/** {@code sequence(10, batch_size := 0)}: a failed argument constraint is the caller's fault. */
69+
@Test
70+
void argumentConstraintIsInvalidArgument() {
71+
byte[] args = ArgumentsEncoder.builder().positional(10L).named("batch_size", 0L).encode();
72+
RpcError e = assertThrows(RpcError.class, () -> vgi.bind(tableBind("sequence", args), null));
73+
assertEquals(Code.INVALID_ARGUMENT.name(), e.errorCode(), e.errorMessage());
74+
assertTrue(e.errorMessage().contains("must be >= 1"), e.errorMessage());
75+
}
76+
77+
/** Binding a function the worker does not serve is a failed lookup. */
78+
@Test
79+
void unknownFunctionIsNotFound() {
80+
RpcError e = assertThrows(RpcError.class, () -> vgi.bind(
81+
tableBind("no_such_function_at_all", ArgumentsEncoder.positionalArgs(1L)), null));
82+
assertEquals(Code.NOT_FOUND.name(), e.errorCode(), e.errorMessage());
83+
}
84+
85+
/** DDL against the example catalog, which is not writable, is refused as a precondition. */
86+
@Test
87+
void ddlOnReadOnlyCatalogIsFailedPrecondition() {
88+
RpcError e = assertThrows(RpcError.class, () -> vgi.catalog_schema_create(
89+
handle, List.of("nope"), "error", null, java.util.Map.of(), null, null));
90+
assertEquals(Code.FAILED_PRECONDITION.name(), e.errorCode(), e.errorMessage());
91+
assertTrue(e.errorMessage().contains("read-only"), e.errorMessage());
92+
}
3493
}

‎vgi/src/main/java/farm/query/vgi/CatalogInterface.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ default CatalogContentsResult catalogContents(CatalogContentsProvider.Request re
310310
* @return the exception
311311
*/
312312
private UnsupportedOperationException readOnly(String method) {
313-
return new UnsupportedOperationException(
313+
return new ReadOnlyCatalogException(
314314
"catalog " + name() + " is read-only: " + method + " not supported");
315315
}
316316

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi;
4+
5+
import farm.query.vgirpc.errors.Code;
6+
import farm.query.vgirpc.errors.HasErrorCode;
7+
8+
/**
9+
* The caller's input was wrong: an argument failed validation, had a bad value, or an argument or input column has a type the function rejects.
10+
*
11+
* <p>Reported on the wire with {@code error_code = INVALID_ARGUMENT} (vgi-rpc error model, WIRE_PROTOCOL
12+
* §8), so a client can tell this failure from a worker bug. It extends
13+
* {@link IllegalArgumentException}, the JDK type the SDK threw here before, so existing {@code catch} clauses
14+
* keep working. No {@code error_kind} is set.
15+
*/
16+
public class InvalidArgumentException extends IllegalArgumentException implements HasErrorCode {
17+
18+
private static final long serialVersionUID = 1L;
19+
20+
/**
21+
* Build the error.
22+
*
23+
* @param message the human-readable message, reported verbatim
24+
*/
25+
public InvalidArgumentException(String message) {
26+
super(message);
27+
}
28+
29+
/**
30+
* Build the error with an underlying cause.
31+
*
32+
* @param message the human-readable message, reported verbatim
33+
* @param cause the underlying failure
34+
*/
35+
public InvalidArgumentException(String message, Throwable cause) {
36+
super(message, cause);
37+
}
38+
39+
/** @return {@link Code#INVALID_ARGUMENT} */
40+
@Override
41+
public Code errorCode() {
42+
return Code.INVALID_ARGUMENT;
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi;
4+
5+
import farm.query.vgirpc.errors.Code;
6+
import farm.query.vgirpc.errors.HasErrorCode;
7+
8+
/**
9+
* A lookup by name failed: no such function, table, schema, catalog, or other object.
10+
*
11+
* <p>Reported on the wire with {@code error_code = NOT_FOUND} (vgi-rpc error model, WIRE_PROTOCOL
12+
* §8), so a client can tell this failure from a worker bug. It extends
13+
* {@link IllegalArgumentException}, the JDK type the SDK threw here before, so existing {@code catch} clauses
14+
* keep working. No {@code error_kind} is set.
15+
*/
16+
public class NotFoundException extends IllegalArgumentException implements HasErrorCode {
17+
18+
private static final long serialVersionUID = 1L;
19+
20+
/**
21+
* Build the error.
22+
*
23+
* @param message the human-readable message, reported verbatim
24+
*/
25+
public NotFoundException(String message) {
26+
super(message);
27+
}
28+
29+
/**
30+
* Build the error with an underlying cause.
31+
*
32+
* @param message the human-readable message, reported verbatim
33+
* @param cause the underlying failure
34+
*/
35+
public NotFoundException(String message, Throwable cause) {
36+
super(message, cause);
37+
}
38+
39+
/** @return {@link Code#NOT_FOUND} */
40+
@Override
41+
public Code errorCode() {
42+
return Code.NOT_FOUND;
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi;
4+
5+
import farm.query.vgirpc.errors.Code;
6+
import farm.query.vgirpc.errors.HasErrorCode;
7+
8+
/**
9+
* A write (DDL or DML) was sent to a catalog that is read-only.
10+
*
11+
* <p>Reported on the wire with {@code error_code = FAILED_PRECONDITION} (vgi-rpc error model, WIRE_PROTOCOL
12+
* §8), so a client can tell this failure from a worker bug. It extends
13+
* {@link UnsupportedOperationException}, the JDK type the SDK threw here before, so existing {@code catch} clauses
14+
* keep working. No {@code error_kind} is set.
15+
*/
16+
public class ReadOnlyCatalogException extends UnsupportedOperationException implements HasErrorCode {
17+
18+
private static final long serialVersionUID = 1L;
19+
20+
/**
21+
* Build the error.
22+
*
23+
* @param message the human-readable message, reported verbatim
24+
*/
25+
public ReadOnlyCatalogException(String message) {
26+
super(message);
27+
}
28+
29+
/**
30+
* Build the error with an underlying cause.
31+
*
32+
* @param message the human-readable message, reported verbatim
33+
* @param cause the underlying failure
34+
*/
35+
public ReadOnlyCatalogException(String message, Throwable cause) {
36+
super(message, cause);
37+
}
38+
39+
/** @return {@link Code#FAILED_PRECONDITION} */
40+
@Override
41+
public Code errorCode() {
42+
return Code.FAILED_PRECONDITION;
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi;
4+
5+
import farm.query.vgirpc.errors.Code;
6+
import farm.query.vgirpc.errors.HasErrorCode;
7+
8+
/**
9+
* An operation the SDK explicitly does not support.
10+
*
11+
* <p>Reported on the wire with {@code error_code = UNIMPLEMENTED} (vgi-rpc error model, WIRE_PROTOCOL
12+
* §8), so a client can tell this failure from a worker bug. It extends
13+
* {@link UnsupportedOperationException}, the JDK type the SDK threw here before, so existing {@code catch} clauses
14+
* keep working. No {@code error_kind} is set.
15+
*/
16+
public class UnimplementedException extends UnsupportedOperationException implements HasErrorCode {
17+
18+
private static final long serialVersionUID = 1L;
19+
20+
/**
21+
* Build the error.
22+
*
23+
* @param message the human-readable message, reported verbatim
24+
*/
25+
public UnimplementedException(String message) {
26+
super(message);
27+
}
28+
29+
/**
30+
* Build the error with an underlying cause.
31+
*
32+
* @param message the human-readable message, reported verbatim
33+
* @param cause the underlying failure
34+
*/
35+
public UnimplementedException(String message, Throwable cause) {
36+
super(message, cause);
37+
}
38+
39+
/** @return {@link Code#UNIMPLEMENTED} */
40+
@Override
41+
public Code errorCode() {
42+
return Code.UNIMPLEMENTED;
43+
}
44+
}

‎vgi/src/main/java/farm/query/vgi/catalog/InMemoryCatalog.java‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package farm.query.vgi.catalog;
44

55
import farm.query.vgi.CatalogInterface;
6+
import farm.query.vgi.NotFoundException;
67
import farm.query.vgi.protocol.CatalogAttachRequest;
78
import farm.query.vgi.protocol.SchemaInfo;
89
import farm.query.vgi.protocol.TableCreateRequest;
@@ -204,7 +205,7 @@ public void schemaCreate(byte[] attachId, List<String> path, OnConflict onConfli
204205
}
205206
}
206207
if (path.size() > 1 && !s.schemas.containsKey(key(path.subList(0, path.size() - 1)))) {
207-
throw new IllegalArgumentException(
208+
throw new NotFoundException(
208209
"Schema " + String.join(".", path.subList(0, path.size() - 1)) + " not found");
209210
}
210211
s.schemas.put(key(path), new SchemaState(path, comment, tags));
@@ -219,7 +220,7 @@ public void schemaDrop(byte[] attachId, List<String> path, boolean ignoreNotFoun
219220
SchemaState schema = s.schemas.get(key(path));
220221
if (schema == null) {
221222
if (ignoreNotFound) return;
222-
throw new IllegalArgumentException("Schema " + String.join(".", path) + " not found");
223+
throw new NotFoundException("Schema " + String.join(".", path) + " not found");
223224
}
224225
List<List<String>> doomed = new ArrayList<>();
225226
for (Map.Entry<List<String>, SchemaState> e : s.schemas.entrySet()) {
@@ -265,7 +266,7 @@ public void tableDrop(byte[] attachId, List<String> path, String name, boolean i
265266
SchemaState schema = s.schemas.get(key(path));
266267
if (schema == null || schema.tables.remove(name) == null) {
267268
if (ignoreNotFound) return;
268-
throw new IllegalArgumentException("Table " + name + " not found in schema " + String.join(".", path));
269+
throw new NotFoundException("Table " + name + " not found in schema " + String.join(".", path));
269270
}
270271
s.version++;
271272
}
@@ -291,7 +292,7 @@ public void viewDrop(byte[] attachId, List<String> path, String name, boolean ig
291292
SchemaState schema = s.schemas.get(key(path));
292293
if (schema == null || schema.views.remove(name) == null) {
293294
if (ignoreNotFound) return;
294-
throw new IllegalArgumentException("View " + name + " not found in schema " + String.join(".", path));
295+
throw new NotFoundException("View " + name + " not found in schema " + String.join(".", path));
295296
}
296297
s.version++;
297298
}
@@ -311,7 +312,7 @@ private State state(byte[] attachId) {
311312

312313
private static SchemaState requireSchema(State s, List<String> path) {
313314
SchemaState schema = s.schemas.get(key(path));
314-
if (schema == null) throw new IllegalArgumentException("Schema " + String.join(".", path) + " not found");
315+
if (schema == null) throw new NotFoundException("Schema " + String.join(".", path) + " not found");
315316
return schema;
316317
}
317318

0 commit comments

Comments
 (0)