Repository navigation
Binary fields answer their base64 text, not their bytes #421
Description
An Elasticsearch binary field stores bytes, and every Elasticsearch API carries them as a base64 string (_source included). Core, the Arrow layer (softclient4es-arrow) and the JDBC driver (softclient4es-jdbc) answer that base64 text for the field, as a string or as the UTF-8 bytes of the string, never the bytes the field stores. So a VARBINARY column answers other bytes than a SQL engine holding the same data, and the bytes written through JDBC's setBytes are not the bytes getBytes reads back.
Measured
Core, through the gateway, on Elasticsearch 8.18.3. Table t (id KEYWORD, s KEYWORD, bin VARBINARY), three documents:
bin=YWI=(the bytes0x6162, "ab"),s=a;bin=eA==(the byte0x78, "x"),s=b;- no
bin,s=c.
DuckDB 1.5.5 was given the same data as a BLOB holding the stored bytes.
| statement | core main (7fe1d66) |
core with #420 | DuckDB 1.5.5 |
|---|---|---|---|
SELECT bin FROM t |
"YWI=", "eA==", NULL: strings, inferred VARCHAR |
the same | 0x6162, 0x78, NULL: BLOB |
SELECT bin FROM t UNION ALL SELECT bin FROM t |
strings, inferred VARCHAR |
the same | BLOB, the stored bytes |
SELECT bin FROM t UNION ALL SELECT NULL FROM t |
strings, inferred VARCHAR |
the same | BLOB, the stored bytes |
SELECT bin FROM t UNION ALL SELECT s FROM t |
refused (VARBINARY beside KEYWORD) |
0x5957493d (the text YWI=), 0x65413d3d (eA==), 0x61, 0x62, 0x63, NULL: byte[], inferred VARBINARY |
0x6162, 0x78, 0x61, 0x62, 0x63, NULL: BLOB |
Given the bytes of the base64 text instead (what the Arrow layer hands it today, below), DuckDB answers 0x5957493d, 0x65413d3d: the base64 text, as core does.
What each layer hands out
Read at the current main of each repository.
- Core (SoftClient4ES
main, 7fe1d66):core/src/main/scala/app/softnetwork/elastic/client/ElasticConversion.scala:1443-1446(jsonNodeToAny) answers a text node as its text, whatever the column's type: abinaryfield is its base64String.- Core's own contract for a
VARBINARYcell isbyte[]:sql/src/main/scala/app/softnetwork/elastic/sql/type/ValueCoercion.scala:55infersVARBINARYfromArray[Byte];core/src/main/scala/app/softnetwork/elastic/client/metadata/JdbcTypeCatalog.scala:65reportsTypes.VARBINARY;core/src/main/scala/app/softnetwork/elastic/client/metadata/JavaValueConversion.scala:66documents thebyte[]cell.
- With fix(sql): set operations take DuckDB's types and answer one class per column; aggregates without GROUP BY answer one row #420, where a set operation types a
VARBINARYbeside text asVARBINARY, the base64Stringbecomes its UTF-8 bytes (a column, an object's field, a list's element), to agree with the two layers below.
- Arrow (softclient4es-arrow
main, 264eac3),arrow-core/src/main/scala/app/softnetwork/elastic/arrow/ArrowTypeMapping.scala::234-235mapsVARBINARYto ArrowBinary;:288-293writes a value that is not abyte[]asother.toString.getBytes(StandardCharsets.UTF_8): the bytes of the base64 text, which DuckDB then holds as itsBLOB;:716-717reads them back unchanged.
- JDBC (softclient4es-jdbc
main, 39c1434), underdriver/src/main/scala/app/softnetwork/elastic/jdbc/:getBytesanswers aStringcell ass.getBytes("UTF-8"), inElasticResultSet.scala:250-259and inElasticStreamingResultSet.scala:233-242;setBytes(ElasticPreparedStatement.scala:437-438) binds abyte[], rendered as its base64 literal (:202).- So
setByteswith0x6162storesYWI=, andgetBytesreads back0x5957493d.
Expected
A binary field answers the bytes it stores, as SQL engines do, in core, over Arrow and through JDBC:
SELECT bin FROM tanswersbyte[]0x6162,0x78, NULL, inferred and reportedVARBINARY;- the same for
bin UNION ALL binandbin UNION ALL NULL; - in
bin UNION ALL sthe binary rows are the stored bytes, and a text row is cast to bytes as DuckDB casts aVARCHARto aBLOB.
Fix direction
Decode the base64 into the stored bytes in one place, and move the three layers together:
- Core: answer a
VARBINARYcell (the schema says which columns arebinary) asbyte[]decoded from its base64, in a single statement and on every route a set operation takes. TheUNION ALLconversion then uses that decoding instead of the UTF-8 bytes of the text. - Arrow:
VarBinaryVectorthen receivesbyte[]from core, which itsbyte[]arm already writes unchanged. - JDBC:
getBytesthen receivesbyte[].setByteskeeps writing base64, so the value written is the value read back.
Changing one layer alone only moves the mismatch between layers, so the three should ship in the same release.
Activity