Skip to content

BUG: gRPC ExecuteQuery drops null-valued projected columns from the record, while HTTP SELECT keeps them as JSON null #4692

Description

@TheRealMcoy

Description

When a SELECT projects a column whose value is null for a given row, the two transports disagree on the result shape:

  • HTTP /api/v1/command/<db> returns the column with an explicit null: {"r": null}.
  • gRPC ExecuteQuery omits the column from GrpcRecord.properties entirely - the key is simply absent.

A client cannot tell, from the gRPC response alone, the difference between "the query did not project this column" and "the query projected it but the value was null." The column name is lost. Any client that addresses result columns by key sees a missing-key error on gRPC for a query that succeeds on HTTP.

sqrt(<negative>) is a convenient trigger (ArcadeDB returns null rather than raising), but the bug is general to any null-valued projection: ifnull fallbacks, coalesce over all-null inputs, a projected property that is unset on the record, etc.

Environment

  • ArcadeDB Server 26.7.1-SNAPSHOT (build a721d414aa646a264b51ae9405cb70708046d801/1781933565482)
  • gRPC ExecuteQuery vs HTTP /api/v1/command/<db>, language sql
  • Default database options

Root cause

ArcadeDbGrpcService.convertResultToGrpcRecord skips any property whose value is null when building the record's properties map:

grpcw/src/main/java/com/arcadedb/server/grpc/ArcadeDbGrpcService.java:3025-3043

// Iterate over ALL properties from the Result, including aliases
for (String propertyName : result.getPropertyNames()) {
  Object value = result.getProperty(propertyName);

  if (value != null) {                       // <-- null-valued columns never reach putProperties
    ...
    GrpcValue gv = projectionConfig != null ?
        toGrpcValue(value, projectionConfig) :
        toGrpcValue(value);
    ...
    builder.putProperties(propertyName, gv);
  }
}

result.getPropertyNames() does include the projected column (the alias is present), but the if (value != null) guard drops it, so the key never lands in the proto map. This is the method on the ExecuteQuery path (ArcadeDbGrpcService.java:937 calls it).

This is not recoverable on the client side: the proto GrpcValue oneof already has an unset/None representation, so a null could be carried faithfully - but the server never emits an entry to carry it. The absence of the key, not a malformed value, is the bug.

By contrast, the HTTP path serializes through JSONObject, which writes "<alias>": null for a null value, preserving the column name.

Steps to reproduce

HTTP (shows the correct, key-preserving behavior):

curl -sS -u root:playwithdata http://localhost:2480/api/v1/server \
  -H 'Content-Type: application/json' \
  -d '{"command":"create database bugscratch"}'

# null-valued projection: sqrt of a negative yields null
curl -sS -u root:playwithdata http://localhost:2480/api/v1/command/bugscratch \
  -H 'Content-Type: application/json' \
  -d '{"language":"sql","command":"SELECT sqrt(-4) AS r"}'
# -> {"user":"root","result":[{"r":null}]}     <-- key "r" present, value null

# control: a non-null value
curl -sS -u root:playwithdata http://localhost:2480/api/v1/command/bugscratch \
  -H 'Content-Type: application/json' \
  -d '{"language":"sql","command":"SELECT sqrt(16) AS r"}'
# -> {"user":"root","result":[{"r":4, ...}]}

gRPC (shows the bug) - add to grpcw/src/test/java/com/arcadedb/server/grpc/GrpcServerIT.java, mirroring executeQuerySelectsExistingData:

@Test
void executeQueryKeepsNullValuedProjectedColumn() {
  ExecuteQueryRequest request = ExecuteQueryRequest.newBuilder()
      .setDatabase(getDatabaseName())
      .setCredentials(credentials())
      .setQuery("SELECT sqrt(-4) AS r")
      .build();

  ExecuteQueryResponse response = authenticatedStub.executeQuery(request);

  GrpcRecord record = response.getResultsList().get(0).getRecordsList().get(0);

  // FAILS today: the key "r" is absent because the null value was skipped.
  assertThat(record.getPropertiesMap()).containsKey("r");
}

Observed

  • HTTP: {"r": null} - the projected alias r is present with a null value.
  • gRPC: GrpcRecord.properties has no r entry; getPropertiesMap().containsKey("r") is false.

So the same query yields a column on HTTP and no column on gRPC.

Expected

The two transports should agree on the projected column set. A column that is projected but null should appear in the gRPC GrpcRecord.properties map with a null/unset GrpcValue, matching the HTTP serializer's "r": null. Clients should be able to address every projected alias by key regardless of transport.

Suggested fix

Pin the projected column set to getPropertyNames() and carry nulls explicitly, rather than dropping them. Two high-level options:

  1. Emit an explicit null GrpcValue for null-valued columns (preferred - matches HTTP). Drop the if (value != null) guard in convertResultToGrpcRecord and let toGrpcValue(null) produce the unset/null-kind GrpcValue (the converter at GrpcTypeConverter.toGrpcValue already has an o == null branch). Every projected alias then appears in the map, and the Python/Java clients surface it as None/null.

    for (String propertyName : result.getPropertyNames()) {
      final Object value = result.getProperty(propertyName);
      final GrpcValue gv = projectionConfig != null ? toGrpcValue(value, projectionConfig) : toGrpcValue(value);
      builder.putProperties(propertyName, gv);
    }

    Verify toGrpcValue(null) yields a value whose WhichOneof("kind") is unset (or a dedicated null kind) so the wire stays compact.

  2. Add an explicit null-kind to the GrpcValue oneof if a distinct "present-but-null" marker is wanted on the wire (heavier; only if option 1's unset-kind round-trip proves ambiguous against genuinely-absent keys elsewhere).

Option 1 is the right fix: it makes gRPC match the long-standing HTTP contract with a one-line guard removal, and the client-side null mapping already exists. The same if (value != null) pattern appears in the streaming/batch record builders (convertPropToGrpcValue call sites around ArcadeDbGrpcService.java:1328/1382/1448); audit those for the same drop so all ExecuteQuery-family paths agree.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions