Trino version bands and connector compatibility¶
Status: implemented and tested on Trino 482 and 484 (both bands green).
Why this document exists¶
Trino publishes a release roughly every two weeks, offers no LTS, and makes no binary or source compatibility promise for the connector SPI. A plugin built against one release is expected to be rebuilt for the next. The practical question is therefore not “does it work on every version”, but “how many distinct source shapes must we maintain, and which ones do real deployments run”.
This document records the answer from measurement, not from documentation.
How the bands were measured¶
Boundaries were read directly from the published trino-spi jars:
for v in 468 470 472 474 476 478 480 482 484; do
curl -sO https://repo1.maven.org/maven2/io/trino/trino-spi/$v/trino-spi-$v.jar
echo "=== $v ==="
javap -cp trino-spi-$v.jar io.trino.spi.connector.ConnectorMetadata | grep finishMerge
javap -cp trino-spi-$v.jar io.trino.spi.connector.ConnectorPageSource | grep getNext
javap -cp trino-spi-$v.jar io.trino.spi.connector.ConnectorSplitManager | grep getSplits
javap -cp trino-spi-$v.jar io.trino.spi.connector.ConnectorPageSinkProvider | grep createMergeSink
done
Measured result¶
Band |
Versions |
|
|
|
|
|---|---|---|---|---|---|
A |
<= 473 |
|
|
|
5 args |
B |
474-477 |
both (deprecation window) |
|
|
5 args |
C |
478-481 |
|
|
|
5 args |
D |
482-483 |
|
|
|
5 args |
E |
484+ |
|
|
|
6 args ( |
Two things stand out.
Boundaries cluster at 474, 478 and 482 — one every four releases, about every eight weeks. Trino follows a consistent add-default, deprecate, delete cadence with a four-release window:
getNextSourcePageappears in 474 andgetNextPageis gone in 478.484 breaks the rhythm because it bundles a return-type sweep (
void->Optional). A return type cannot be deprecation-windowed at all, so that change lands with no migration path for the old signature.
Supported bands¶
We ship two artifacts, which covers essentially every real deployment:
Band |
Versions |
Role |
Coverage |
|---|---|---|---|
|
482-483 |
current-minus |
compile + full test suite |
|
484+ |
current and forward |
compile + full test suite |
Band e is the default. The 468 claim in older docs has been dropped: it is
band A, four breaking changes behind, and no longer supported.
Build¶
# Band e (default), Trino 484
mvn package
# Band d, Trino 482
mvn package -Dtrino.version=482 -Dtrino.band=d
# Deprecated aliases kept for existing scripts
mvn package -P trino-482
mvn package -P trino-484
CI matrix¶
for v in 482 484; do
band=d; [ "$v" = 484 ] && band=e
mvn -B test -Dtrino.version=$v -Dtrino.band=$band
done
How the split is implemented¶
Only the members that genuinely differ are band-specific. Everything else stays in one shared source tree so logic can never silently diverge.
Differing member |
Shared base |
Band |
Band |
|---|---|---|---|
|
|
returns |
returns |
|
|
5 args |
6 args with |
Band shells live in src/main/java-trino-d and src/main/java-trino-e, and are
wired in by build-helper-maven-plugin using the trino.band property.
Verified non-seams¶
These were checked and are identical in bands D and E, so they stay shared:
ConnectorSplitManager.getSplits(..., Set<ColumnHandle>, Constraint)ConnectorSplitSource.getNextBatch(int, DynamicFilterSnapshot)— note the 1-argument form used to exist; 468-era test code callinggetNextBatch(1000)does not compile on either supported bandConnectorPageSourceProvider.createPageSource— both the 8 and 9 argument overloads are still declared, so either can be implementedConnectorSplit— carriesisRemotelyAccessible,getAddresses,getAffinityKey,getSplitWeight,getRetainedSizeInBytes. ThegetSplitInfo()accessor asserted by the old test no longer exists in either band, and that test was removed rather than pinned.ConnectorSplitManager.getSplits5-argument form withConstraintConnectorPageSinkProvider.createPageSink(both overloads)Procedureargument registration rules (see below)
Runtime requirements¶
JVM flags¶
Trino 484’s io.trino.simd.BlockEncodingSimdSupport references
jdk.incubator.vector.VectorShape. That class exists on JDK 25 but only inside
the jdk.incubator.vector incubator module, which is not resolved by
default. Without the flag the engine dies during Guice provisioning with:
java.lang.NoClassDefFoundError: jdk/incubator/vector/VectorShape
at io.trino.simd.BlockEncodingSimdSupport.detectSimd
Trino’s own launcher adds the module. Any embedder (surefire, a test harness, a custom launcher) must match:
--add-modules=jdk.incubator.vector
--add-opens=java.base/java.nio=ALL-UNNAMED # Arrow Java memory access
Java version¶
The SPI is compiled for Java 23, so build and run with JDK 23 or newer.
java.release=23. JDK 25 is what CI uses.
Procedure arguments¶
Two independent validations apply to a VARCHAR procedure argument, and they
pull in opposite directions:
Procedure.Argumentvalidates a declared default value againsttype.getJavaType(), which isSlicefor varchar. Defaults must beSlices.utf8Slice(...).the CALL execution path supplies
Stringvalues and rejects a method handle whose parameter type isSlicewithArgument 'TABLE' has invalid type io.airlift.slice.Slice (expected java.lang.String).
So procedure methods take String, and their registered defaults are Slice.
Both requirements are satisfied simultaneously; see BenoStreamProcedures.
Test stack¶
JUnit is pinned to 6.1.3 via the junit-bom. trino-testing and
trino-main pull JUnit 6 transitively, so declaring JUnit 5 leaves
junit-platform-engine at 1.11.4 beside a JUnit 6 API that references
org.junit.platform.engine.OutputDirectoryCreator, and the forked JVM fails to
load. Surefire is 3.6.0; 3.2.5 predates Platform 6 and mis-detects it. The
junit-vintage-engine is present because TrinoConnectorTest is still JUnit 4,
and without it surefire silently runs zero tests from that class.
Jackson follows the Trino release’s airbase pin (2.22.3 for 484). Since Jackson
2.20, jackson-annotations dropped the patch component, so the valid
coordinate is 2.22, not 2.22.3.
Real-catalog join testing¶
TestCrossCatalogJoin runs inside a real DistributedQueryRunner with a second
first-party catalog and asserts three things:
a scalar join between a BenoStreamDB table and a foreign catalog
a
system.vector_searchresult joined directly against a foreign catalog, with no intermediate materialisationa cross-catalog aggregate
Why tpch and not PostgreSQL for the in-process tier¶
trino-postgresql is not published to Maven Central past 476; the JDBC
connectors now ship only inside the server tarball. Pinning
trino-postgresql:476 against a 482/484 engine would mean testing a band A/B
plugin against a band D/E core, which cannot link. trino-tpch, by contrast, is
published at every release and is pinned to ${trino.version}, so the test
always exercises a plugin built for exactly the engine under test.
A real PostgreSQL join test therefore belongs in a server-distribution tier:
download the Trino server tarball for the target version, use the bundling
plugin/postgresql directory via DistributedQueryRunner.installPlugin(Path)
or run the server and reach it over JDBC, and point it at a Postgres container.
That tier is not yet implemented.
Iceberg zero-copy join¶
BenoStreamDB writes a custom _manifest/v2.json layout, not standard Iceberg
metadata. Trino’s Iceberg connector reads standard metadata only, so it cannot
join our tables directly today. Zero-copy join with a real Iceberg catalog
requires one of:
an external-table overlay in the Rust engine (register a table whose storage is an existing Iceberg table), or
emitting standard Iceberg metadata so the same files are readable by both Trino’s Iceberg connector and our engine.
Neither is implemented. Until then, zero-copy claims should be limited to “reads the Parquet files in place” and must not be described as Iceberg catalog interoperability.
Scalar functions: how they are contributed (solved)¶
Spark registers seven SQL UDFs. Trino now exposes all seven, but not through the connector, and the difference matters.
The wrong path¶
ConnectorMetadata exposes listFunctions, getFunctions and
getFunctionMetadata, plus Connector.getFunctionProvider(). Implementing all
of them compiles and looks correct, but Trino never calls listFunctions
for a catalog created at runtime (instrumenting it produced a call count of 0),
so nothing is ever enumerated and every call fails with
Function '<name>' not registered. No amount of FunctionId or naming fiddling
fixes that, which is why the earlier investigation looked so erratic.
A red herring along the way: cosine_distance and dot_product did resolve.
They were not ours. Trino 484 ships both as built-ins, together with
euclidean_distance and hamming_distance. That was confirmed by the plugin
failing to install with
Function already registered: cosine_distance(array(double),array(double)):double.
The correct path¶
Per Trino’s developer guide, scalar functions are contributed by the plugin:
@Override
public Set<Class<?>> getFunctions() {
return Set.of(BenoStreamScalarFunctions.class);
}
The class is annotated with @ScalarFunction, @Description and @SqlType,
and the engine derives the signatures. Two non-obvious requirements:
An
array(...)argument arrives as a TrinoBlock, not aList. DeclaringListfails at specialization withjava types do not match.Plugin functions land in the global
system.builtinnamespace, so they cannot shadow a built-in and cannot be scoped to a catalog or schema. There is no per-schema overloading in Trino.
Resulting parity map¶
Spark UDF |
Trino |
Notes |
|---|---|---|
|
built-in |
cannot be overridden |
|
built-in |
cannot be overridden |
|
ours |
equivalent to built-in |
|
ours |
metric dispatch; |
|
ours |
merge-join over index arrays |
|
ours |
alpha clamped to [0,1] |
|
ours |
sums 1/(k+rank) over positive ranks |
TestBenoStreamDBQueries asserts that the five registered functions are visible
and evaluate correctly, and pins the semantics of the built-ins (including that
opposing vectors yield 2.0 for cosine, i.e. no cap at 1).
Getting engine semantics on our data¶
Because a built-in cannot be overridden per catalog, engine-side scoring for our
tables comes from pushdown instead: system.vector_search and
system.hybrid_search return distance / rrf_score computed inside the Rust
session. A hand-written cosine_distance(col, ...) on a BenoStreamDB table is
evaluated by Trino. If matching Spark bit-for-bit matters for that case, the
path is a custom Trino type (Plugin.getTypes()) so the function has a distinct
signature rather than a shadowed one.
Test-harness note¶
add_index registers an index and builds it on a detached background task.
A query issued immediately afterwards can observe no index, which made the
hybrid test flaky. Tests call build_index explicitly; the two-phase contract
should be documented for users as well.
Refresh procedure¶
When a new Trino release appears:
Check
https://repo1.maven.org/maven2/io/trino/trino-spi/maven-metadata.xmlfor<release>.Diff the SPI seams with the
javaploop above.If nothing moved, add the version to the existing band and to the CI matrix.
If a seam moved, add a new band directory with the differing shell only.
Never claim a version is supported because it compiled once; every band in the matrix must run the full suite.