This is an automated email from the ASF dual-hosted git repository.
voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 0db52460cfa6 test(trino): add a Trino E2E testcontainers pipeline for
the RFC-105 connector (#19217)
0db52460cfa6 is described below
commit 0db52460cfa6a5ce74115e43f75709a99821fee2
Author: voonhous <[email protected]>
AuthorDate: Thu Aug 6 10:03:57 2026 +0800
test(trino): add a Trino E2E testcontainers pipeline for the RFC-105
connector (#19217)
* test(integ-test): add Trino E2E harness for the RFC-105 connector
Revives the testcontainers Trino harness from the stale branch, adapted:
- TrinoService + ITTestTrino{Smoke,StockTicks,CustomType}, gated on a new
docker-compose 'trino' profile (-Dcompose.profiles=trino) so existing
hive-sync CI rows are unaffected
- docker/trino: Dockerfile (FROM trinodb/trino:481, bakes the plugin dir
built from the Trino repo's thin trino-hudi shim + etc configs),
overlay-aware entrypoint (jar-bearing TRINO_PLUGIN_DIR mount overrides the
baked plugin for fast iteration), build_image.sh
- trinocoordinator service (profile-gated) in the spark402 compose pair
* ci(trino): complete the Trino E2E pipeline for the RFC-105 connector
The harness from the previous commit could not actually run: nothing in
the repo could assemble the Trino plugin directory (the RFC-105 shim is
not yet released upstream), the stock-ticks seed fixture was never
committed, and no workflow activated the trino compose profile.
- docker/trino/shim: standalone maven project mirroring the upstream
trinodb/trino plugin/trino-hudi shim (parent io.trino:trino-root:481
from Maven Central, packaging trino-plugin, depends on
org.apache.hudi:hudi-trino). Carries its own thin HudiPlugin class
because trino-maven-plugin's service descriptor generator only scans
the module's own classes; never deployed (maven.deploy.skip). Build
with package, never install.
- docker/trino/Dockerfile + overlay-entrypoint.sh: preserve the stock
image's plugin/hudi/hdfs loader jars at /opt/hudi-hdfs-lib and
re-attach them when the baked or overlaid plugin dir lacks them
(shim-assembled dirs always do; io.trino:trino-hdfs:zip is not on
Maven Central, it only ships in the server tarball / base image).
- docker/demo/sparksql-stock-ticks-trino.commands: the COW + MOR seed
fixture ITTestTrinoStockTicks references, jdbc-mode hive sync like
the existing custom-type fixtures.
- .github/workflows/hudi_trino_e2e.yml: JDK 17 reactor build, JDK 25
connector + shim build, local image build via build_image.sh, then
ITTestTrino* against the spark402 compose stack with
-Dcompose.profiles=trino. No trinodb/trino checkout and no DockerHub
publish anywhere in the flow.
- docker/trino/.gitignore: anchor plugin/ to /plugin/ so it does not
swallow the shim's io/trino/plugin source package.
- READMEs: local E2E flow + overlay fast loop; drop stale
hudi-trino-plugin path comments.
* ci(trino): TEMP: push-trigger the E2E workflow on the dev branch
workflow_dispatch cannot target a workflow that does not exist on the
default branch yet. Drop this commit before merging upstream.
* fix(trino): drop jts-core provided override in the E2E shim pom
Trino 481's SpiDependencyChecker rejects provided scope for
org.locationtech.jts:jts-core: it is not part of the 481 SPI surface
(trino-spi 481 depends only on jackson-annotations, slice, and the
opentelemetry api/api-incubator/common/context set). jts-core now flows
into the assembled plugin dir at its transitive scope. The upstream
482-SNAPSHOT shim declares it provided because the SPI gains jts there.
* test(integ-test): surface Trino boot failures in the E2E pipeline
All three ITTestTrino* classes failed with 'execInContainer can only be
used while the Container is running': the coordinator dies at startup
and nothing captured why (compose teardown discards the container).
- ITTestBaseTestcontainers: stream trinocoordinator logs into the test
output via Slf4jLogConsumer when the trino profile is active, so a
startup crash leaves its root cause in the failsafe report.
- TrinoService.waitUntilReady: fail fast with a pointed message when
the container is no longer running instead of exhausting 18 retries
against a dead container (saves ~3 min per class).
- hudi_trino_e2e.yml: assert the assembled plugin dir is populated and
carries the services jar; add a standalone smoke-boot of the built
image (docker run + SELECT 1 via the CLI, boot log dumped on failure)
so image-level boot failures surface ~30 min before the IT step.
* fix(trino): drop GCLocker JVM flags the Trino 481 JDK no longer accepts
The standalone smoke-boot caught the coordinator crash root cause:
'Unrecognized VM option GCLockerRetryAllocationCount=32'. The flag was
carried over from Hudi's JDK 11/17 CI OOM workaround, but the GCLocker
was removed in modern JDKs, so the JDK 25 JVM inside trinodb/trino:481
refuses to start and the container dies before any Trino code runs --
which is exactly the 'container is not running' failure all three
ITTestTrino* classes hit.
* test(integ-test): surface server-side stacks for failing Trino queries
The E2E run reached real query execution (ITTestTrinoSmoke fully green,
the stock-ticks fixture seeds and syncs correctly) but every table read
fails with 'Failed to generate splits for default.<table>' and only the
one-line message survives into the report.
- TrinoService: run the CLI with --debug so query failures carry the
full server-side stack into the assertion message.
- hudi_trino_e2e.yml: on failure, print the failsafe *-output.txt tails
(where surefire redirects the streamed trinocoordinator logs).
* ci(trino): add the ASF license header to the empty-overlay .gitkeep
The release licensing check (scripts/release/validate_source_copyright.sh)
requires every non-excluded file to contain the ASF header, and an empty
.gitkeep cannot. The file only keeps docker/trino/empty-overlay in git as
the compose default plugin-overlay mount source; its content is ignored at
runtime (the overlay entrypoint only applies an overlay that contains jars),
so the header plus a purpose note satisfies the check without touching the
shared release script.
* test(integ-test): address Trino E2E review on teardown, naming and MOR
signal
- Guard @AfterAll teardown in ITTestTrinoStockTicks/ITTestTrinoCustomType:
JUnit runs @AfterAll even when the @BeforeAll assumption aborts
setupOnce()
before initializeServices(), so a non-trino stack NPE'd on the null spark
service instead of skipping cleanly.
- Rename the BothPartitionsVisible tests: the emptied dt=2024-01-02
partition
is invisible by design, so the old names claimed the opposite of the
check;
pin the invisibility with an absence assertion.
- Give stock_ticks_mor a log-only UPDATE delta so the _ro and _rt views
diverge: _ro must keep serving the base row (now absence-pinned) and two
new _rt tests verify the merged read, so the MOR rows exercise a genuinely
MOR read path instead of the base-file-only path COW already covers.
* ci(trino): harden the Trino E2E workflow and shim per review
- Drop the TEMP dev-branch push trigger: only squash merge is enabled in
.asf.yaml, so there is no separate commit to drop and the line would ship
to master with the branch head.
- Widen the demo paths filter to docker/demo/**: the ITs drive several demo
fixtures beyond the stock-ticks commands file, and edits to those must
re-trigger this pipeline.
- Carry bot.yml's maven.wagon retry flags in MVN_ARGS: the JDK 17 step is
the cold-cache full-reactor build those flags were added for.
- Redirect failsafe test output to files so the on-failure dump step has the
*-output.txt files it tails; failsafe defaults the redirect off, so the
loop never matched anything before.
- Derive dep.hudi.version from the reactor pom when building the shim:
cut_release_branch.sh's versions:set cannot bump the out-of-reactor shim
pom, and a stale literal could resolve silently from the actions maven
cache instead of failing loudly.
- Hard-disable install for the shim pom (maven.install.skip) so a stray
install cannot publish a stub at the real io.trino:trino-hudi coordinates,
and dockerignore the shim tree so shim/target stays out of the docker
build context.
* test(integ-test): retire the legacy trino-coordinator demo path
The trino-coordinator-1 service is gone from every compose file, the
ITTestHoodieDemo trino steps have been commented out since the
HUDI-8269/HUDI-8270 breakage, and the integ2 Trino E2E suite now covers the
same stock-ticks queries against the native RFC-105 connector. Remove the
dead ITTestBase helpers and the ITTestHoodieDemo constants, copy steps and
private test methods; the three trino-*.commands demo files they consumed
were dropped in the preceding fixture commit.
* ci(trino): add the ASF license header to the trino .dockerignore
The release licensing check (scripts/release/validate_source_copyright.sh)
greps every file in the source release tree for the ASF header. The release
rsync (create_source_directory.sh) strips .gitignore files but has no
exclude for .dockerignore, so the file ships in the source release and must
carry the header, same as the empty-overlay .gitkeep. Docker treats leading
# lines as comments, so the ignore behavior is unchanged.
* ci(trino): use bot.yml's aether resolver retry flags and pin the GOOG row
count
- MVN_ARGS carried maven.wagon.* names, which Maven 3.9's native resolver
transport ignores; swap in bot.yml's five aether.connector.http.* flags
verbatim so the 429/5xx retry actually applies to the cold-cache build.
- The _ro/_rt projected-column asserts matched one row substring, which
still
passes when an unmerged base row is returned next to it; pin the symbol
count to one.
* test(integ-test): correct the Trino custom-type javadoc and drop the dead
Spark 3.5 guards
Three doc/scope corrections from review on ITTestTrinoCustomType:
- the class javadoc credited ITTestCustomTypeHiveSync with a re-seeding
@BeforeAll and a cleaning @AfterAll, but it seeds inside each @Test and
cleans in @AfterEach. What actually keeps the two classes from
contaminating each other is tearDownDockerCompose dropping the whole
stack between classes; say that instead.
- trinocoordinator exists only in the spark402 compose pair, so this class
either skips entirely (no trino profile) or runs on a Spark 4.x stack --
isSpark4Compose() is always true when it runs. Drop the conditional
VARIANT seed and the seven assumeSpark4Compose() guards, and replace the
"on Spark 3.5 the BLOB and VECTOR coverage still runs" claim with the
reason VARIANT is unconditional here.
- the two EmptiedPartitionInvisible tests claimed to exercise partition
pruning, but GROUP BY dt only emits groups for partitions that still hold
rows, so it cannot distinguish a connector that prunes from one that
lists -- and dt=2024-01-02 stays registered in the metastore either way.
Reword both comments to state what is actually asserted.
Also trims the now-stale usage guidance on isSpark4Compose(), which no
longer has a @BeforeAll-seeding caller.
* ci(trino): widen the E2E path filter to hadoop.env and fix the
shim/catalog docs
Review round 3 on the Trino E2E pipeline:
- ITTestBaseTestcontainers copies docker/compose/hadoop.env into the
generated
compose dir and all nine services in the spark402 pair load it, but the
workflow path filter matched only the spark402 YAMLs. Add hadoop.env to
both the push and pull_request lists.
- hudi.properties dated the native connector at Trino 472. plugin/trino-hudi
first appears upstream at tag 398 (404 at 397), so correct the number.
- The shim build steps in hudi-trino/README.md and docker/README.md used the
pom's literal dep.hudi.version, which the workflow itself documents as
going stale because cut_release_branch.sh cannot bump a pom outside the
reactor. Derive it from help:evaluate the same way the workflow does.
- Rename Containers.TRINOCOORDINATOR to TRINO_COORDINATOR for consistency
with SPARK_MASTER / ADHOC_1. The value stays "trinocoordinator" to match
the compose service name.
* ci(trino): trigger the E2E pipeline on root pom.xml and all of
hudi-integ-test
Two gaps in the path filter:
- root pom.xml owns trino.version (481), which the shim pom's parent
coordinate, the Dockerfile TRINO_VERSION arg, build_image.sh's default
and four hardcoded trino-hudi-481 paths in this workflow all track by
hand. A bump there needs this pipeline to run; hudi_trino_ci.yml already
lists pom.xml for the same reason.
- the test entry was scoped to integ2/**, but the IT step runs
`mvn verify -pl hudi-integ-test`, which compiles the whole module. This
PR's own edits to integ/ITTestBase and integ/ITTestHoodieDemo sit outside
integ2/, so a break there would fail this pipeline without triggering it.
Widen to hudi-integ-test/**.
Also folds the per-entry comments into one rationale block above the push
list, with the pull_request list pointing at it, so the two stay easy to
diff against each other.
---
.github/workflows/hudi_trino_e2e.yml | 186 +++++++++++
docker/README.md | 27 ++
...r-compose_hadoop340_hive2310_spark402_amd64.yml | 23 ++
...r-compose_hadoop340_hive2310_spark402_arm64.yml | 25 +-
docker/demo/sparksql-stock-ticks-trino.commands | 98 ++++++
docker/demo/trino-batch1.commands | 23 --
docker/demo/trino-batch2-after-compaction.commands | 21 --
docker/demo/trino-table-check.commands | 20 --
docker/trino/.dockerignore | 21 ++
docker/trino/.gitignore | 3 +
docker/trino/Dockerfile | 51 +++
docker/trino/build_image.sh | 77 +++++
docker/trino/empty-overlay/.gitkeep | 21 ++
docker/trino/etc/catalog/hudi.properties | 31 ++
docker/trino/etc/config.properties | 26 ++
docker/trino/etc/hadoop-conf/core-site.xml | 23 ++
docker/trino/etc/hadoop-conf/hdfs-site.xml | 27 ++
docker/trino/etc/jvm.config | 38 +++
docker/trino/etc/node.properties | 22 ++
docker/trino/overlay-entrypoint.sh | 52 +++
docker/trino/shim/pom.xml | 161 +++++++++
.../main/java/io/trino/plugin/hudi/HudiPlugin.java | 39 +++
.../java/org/apache/hudi/integ/ITTestBase.java | 22 --
.../org/apache/hudi/integ/ITTestHoodieDemo.java | 61 +---
.../testcontainers/ITTestBaseTestcontainers.java | 55 ++-
.../testcontainers/TestcontainersConfig.java | 21 ++
.../testcontainers/service/TrinoService.java | 117 +++++++
.../trino/ITTestTrinoCustomType.java | 369 +++++++++++++++++++++
.../testcontainers/trino/ITTestTrinoSmoke.java | 71 ++++
.../trino/ITTestTrinoStockTicks.java | 134 ++++++++
hudi-trino/README.md | 42 +++
31 files changed, 1757 insertions(+), 150 deletions(-)
diff --git a/.github/workflows/hudi_trino_e2e.yml
b/.github/workflows/hudi_trino_e2e.yml
new file mode 100644
index 000000000000..df2f945e36d3
--- /dev/null
+++ b/.github/workflows/hudi_trino_e2e.yml
@@ -0,0 +1,186 @@
+name: Hudi Trino E2E
+
+on:
+ push:
+ branches:
+ - master
+ - 'release-*'
+ paths:
+ # Several entries are deliberately broad -- keep them that way:
+ # docker/demo/** the ITs drive demo fixture scripts
+ # (sparksql-*.commands, setup_demo_container.sh).
+ # docker/compose/hadoop.env
+ # copied into the generated compose dir and
loaded
+ # by every service in the stack.
+ # hudi-integ-test/** the IT step runs `mvn verify -pl
hudi-integ-test`,
+ # which compiles the whole module, so a break
+ # anywhere in it fails this pipeline -- not just
+ # under integ2/.
+ # pom.xml owns trino.version, which the shim pom's
parent,
+ # the Dockerfile TRINO_VERSION arg and the
+ # hardcoded 481 paths below all track by hand.
+ - 'hudi-trino/**'
+ - 'docker/trino/**'
+ - 'docker/compose/docker-compose_hadoop340_hive2310_spark402*'
+ - 'docker/compose/hadoop.env'
+ - 'docker/demo/**'
+ - 'hudi-integ-test/**'
+ - 'pom.xml'
+ - '.github/workflows/hudi_trino_e2e.yml'
+ pull_request:
+ branches:
+ - master
+ - 'release-*'
+ # Keep in sync with the push paths above; see the rationale there.
+ paths:
+ - 'hudi-trino/**'
+ - 'docker/trino/**'
+ - 'docker/compose/docker-compose_hadoop340_hive2310_spark402*'
+ - 'docker/compose/hadoop.env'
+ - 'docker/demo/**'
+ - 'hudi-integ-test/**'
+ - 'pom.xml'
+ - '.github/workflows/hudi_trino_e2e.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: hudi-trino-e2e-${{ github.ref }}
+ cancel-in-progress: ${{ !contains(github.ref, 'master') &&
!contains(github.ref, 'release-') }}
+
+env:
+ # The aether.connector.http.* retry/timeout knobs are copied verbatim from
bot.yml's
+ # MVN_ARGS: the JDK 17 step below is a cold-cache full-reactor build,
exactly what
+ # those were added for. They are maven-resolver properties -- Maven 3.9
resolves through
+ # the native resolver HTTP transport, which ignores the older maven.wagon.*
names.
+ MVN_ARGS: -e -ntp -B -V -Dgpg.skip -Djacoco.skip -Pwarn-log
-Daether.connector.http.retryHandler.count=5
-Daether.connector.http.retryHandler.interval=3000
-Daether.connector.http.retryHandler.intervalMax=30000
-Daether.connector.http.retryHandler.serviceUnavailable=429,500,502,503,504
-Daether.connector.http.connectionMaxTtl=25
+ SCALA_PROFILE: -Dscala-2.13 -Dscala.binary.version=2.13
+ COMPOSE_PREFIX: docker-compose_hadoop340_hive2310_spark402
+
+jobs:
+ trino-e2e:
+ # Testcontainers E2E for the RFC-105 native trino-hudi connector: builds
+ # hudi-trino at HEAD, assembles the plugin dir via the in-repo shim
+ # (docker/trino/shim, standing in for the not-yet-released upstream
+ # trinodb/trino plugin/trino-hudi shim), bakes it into a local
+ # apachehudi/hudi-trino_481 image, and runs ITTestTrino* against the
+ # spark402 compose stack (the only pair with the trinocoordinator service).
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Free disk space
+ run: |
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /usr/local/lib/android
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /usr/local/share/boost
+ docker system prune --all --force --volumes
+ - name: Pre-pull compose images (fails fast if not published)
+ run: |
+ # Surface a missing sparkadhoc image before the long Maven install.
The
+ # remaining stack images are pulled by docker-compose at test time;
the
+ # trino image is built locally below, never pulled.
+ docker pull
apachehudi/hudi-hadoop_3.4.0-hive_2.3.10-sparkadhoc_4.0.2:latest
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ architecture: x64
+ cache: maven
+ - name: Build and install Hudi artifacts (JDK 17)
+ # Full reactor: the compose containers mount the workspace and the
tests
+ # use bundles staged by the -Pintegration-tests build (e.g.
+ # docker/hoodie/hadoop/hive_base/target/hoodie-spark-bundle.jar).
+ run:
+ mvn clean install -T 2 $SCALA_PROFILE -Dspark4.0 -Dflink1.20
-Pintegration-tests -DskipTests=true -Ddocker.compose.skip=true $MVN_ARGS
+ - name: Set up JDK 25
+ uses: actions/setup-java@v5
+ with:
+ java-version: '25'
+ distribution: 'temurin'
+ cache: maven
+ - name: Build hudi-trino connector (JDK 25)
+ # No trinodb/trino checkout needed: the unpublished Trino test-jars sit
+ # behind the off-by-default hudi-trino-tests profile and packaging
+ # resolves entirely from Maven Central.
+ run:
+ mvn $MVN_ARGS -Phudi-trino -pl hudi-trino install
-Dmaven.test.skip=true
+ - name: Assemble trino-hudi plugin dir via in-repo shim (JDK 25)
+ # package, NOT install: installing would shadow the real
+ # io.trino:trino-hudi release coordinates in the local m2 (the shim pom
+ # also hard-disables install via maven.install.skip).
+ # dep.hudi.version is derived from the reactor pom because the shim
sits
+ # outside the reactor: cut_release_branch.sh's `mvn versions:set`
cannot
+ # bump its literal default, and a stale value could resolve silently
+ # from the actions maven cache instead of failing loudly.
+ run: |
+ HUDI_VERSION=$(mvn -q -ntp help:evaluate
-Dexpression=project.version -DforceStdout)
+ echo "Building shim against hudi version: $HUDI_VERSION"
+ mvn $MVN_ARGS -f docker/trino/shim/pom.xml clean package -DskipTests
-Ddep.hudi.version="$HUDI_VERSION"
+ - name: Build apachehudi/hudi-trino_481 image
+ run: |
+ docker/trino/build_image.sh --plugin-dir
docker/trino/shim/target/trino-hudi-481
+ # Sanity: the shim must have produced a populated plugin dir with a
+ # service descriptor jar, or Trino cannot load the plugin at boot.
+ echo "plugin dir jar count: $(ls
docker/trino/shim/target/trino-hudi-481 | wc -l)"
+ ls docker/trino/shim/target/trino-hudi-481/*services*.jar
+ - name: Smoke-boot the Trino image standalone
+ # Catches image-level boot failures (plugin load errors, bad etc/
config)
+ # ~30 min before the IT step would, with the full boot log on screen.
+ # --hostname trinocoordinator makes the baked discovery.uri
self-resolve.
+ run: |
+ docker run -d --name trino-smoke --hostname trinocoordinator \
+ apachehudi/hudi-trino_481:latest
+ ok=""
+ for i in $(seq 1 18); do
+ if [ "$(docker inspect -f '{{.State.Running}}' trino-smoke)" !=
"true" ]; then
+ echo "trino-smoke container died during startup" >&2
+ break
+ fi
+ if docker exec trino-smoke trino --server localhost:8080 \
+ --execute "SELECT 1" >/dev/null 2>&1; then
+ ok=1; echo "Trino answered SELECT 1 (attempt $i)"; break
+ fi
+ sleep 10
+ done
+ if [ -z "$ok" ]; then
+ echo "==== trino-smoke boot log ===="
+ docker logs trino-smoke 2>&1 | tail -200
+ docker rm -f trino-smoke >/dev/null 2>&1 || true
+ exit 1
+ fi
+ docker rm -f trino-smoke
+ - name: Set up JDK 17 (restore for the IT run)
+ # setup-java resets JAVA_HOME on each call; hudi-integ-test needs 17.
+ uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ architecture: x64
+ - name: Run Trino E2E ITs (JDK 17)
+ run: |
+ # -DskipITs=false overrides the spark4.0 profile's skipITs=true
default
+ # (see root pom.xml). -Dcompose.profiles=trino starts the
profile-gated
+ # trinocoordinator service; without it every ITTestTrino* class
skips.
+ # redirectTestOutputToFile makes failsafe write per-class
*-output.txt
+ # files, which the on-failure dump step below relies on.
+ mvn verify $SCALA_PROFILE -Dspark4.0 -Pintegration-tests \
+ -pl hudi-integ-test \
+ -DskipITs=false \
+ -Ddocker.compose.skip=true \
+ -Dit.test='ITTestTrino*' \
+ -Dcompose.profiles=trino \
+ -Dspark.docker.compose.prefix=$COMPOSE_PREFIX \
+ -Dmaven.test.redirectTestOutputToFile=true \
+ $MVN_ARGS
+ - name: Dump failsafe test outputs on failure
+ # The IT step redirects test stdout (incl. the streamed
trinocoordinator
+ # boot/query logs) into per-class output files; print their tails so
+ # server-side failures are readable straight from the workflow log.
+ if: failure()
+ run: |
+ for f in hudi-integ-test/target/failsafe-reports/*-output.txt; do
+ [ -f "$f" ] || continue
+ echo "===== $f (last 400 lines) ====="
+ tail -n 400 "$f"
+ done
diff --git a/docker/README.md b/docker/README.md
index 1563ce76aeb1..8892399fb33f 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -196,3 +196,30 @@ When `--multi-arch` is enabled, the script builds and
pushes the amd64 and arm64
Note that `--multi-arch` uses `docker buildx build --push` and the image names
in the script are hardcoded to the
`apachehudi/...` Docker Hub repositories, so this flow requires push access to
those repositories. No Dockerfile
changes are needed for the current amd64 plus arm64 image set in this
repository.
+
+## Trino E2E image - `/trino`
+
+The Trino E2E stack does not use the `hoodie/hadoop` image tree.
`docker/trino/` builds
+`apachehudi/hudi-trino_<trino-version>` directly on top of the official
`trinodb/trino`
+image, baking in a locally-assembled native `trino-hudi` plugin directory and
the E2E
+catalog config (`connector.name=hudi`, metastore at
`thrift://hivemetastore:9083`).
+
+This image is built locally on demand (also by the `hudi_trino_e2e.yml` CI
workflow) and
+is NOT published to Docker Hub. The plugin directory comes from the in-repo
shim project
+at `docker/trino/shim/` (see `hudi-trino/README.md` for the full build-and-run
flow):
+
+```
+# JDK 25; hudi-trino must already be installed into the local m2
+# dep.hudi.version comes from the reactor pom -- the shim is outside the
reactor, so
+# cut_release_branch.sh cannot bump the literal default in its own pom.
+HUDI_VERSION=$(mvn -q -ntp help:evaluate -Dexpression=project.version
-DforceStdout)
+mvn -f docker/trino/shim/pom.xml clean package -DskipTests
-Ddep.hudi.version="$HUDI_VERSION"
+docker/trino/build_image.sh --plugin-dir
docker/trino/shim/target/trino-hudi-481
+```
+
+The `trinocoordinator` compose service exists only in the
+`docker-compose_hadoop340_hive2310_spark402_{amd64,arm64}.yml` pair, behind
the `trino`
+compose profile, so the default hive-sync flows never start it. For fast plugin
+iteration the container supports a bind-mounted overlay: point
`TRINO_PLUGIN_DIR` (or
+the `-Dtrino.plugin.dir` test property) at a freshly built plugin dir and
restart the
+container instead of rebuilding the image.
diff --git
a/docker/compose/docker-compose_hadoop340_hive2310_spark402_amd64.yml
b/docker/compose/docker-compose_hadoop340_hive2310_spark402_amd64.yml
index 0cd441eef2c5..adf964e2288b 100644
--- a/docker/compose/docker-compose_hadoop340_hive2310_spark402_amd64.yml
+++ b/docker/compose/docker-compose_hadoop340_hive2310_spark402_amd64.yml
@@ -256,6 +256,29 @@ services:
depends_on:
- minio
+ # Gated behind the "trino" compose profile: inert for the default hive-sync
CI
+ # rows, only starts when COMPOSE_PROFILES=trino. The plugin overlay defaults
to
+ # docker/trino/empty-overlay (baked-in plugin used); set TRINO_PLUGIN_DIR to
a
+ # locally-built trino-hudi plugin dir to override it at container start.
+ trinocoordinator:
+ image: apachehudi/hudi-trino_481:latest
+ profiles: ["trino"]
+ hostname: trinocoordinator
+ container_name: trinocoordinator
+ ports:
+ - "8092:8080"
+ depends_on:
+ - "hivemetastore"
+ - "namenode"
+ links:
+ - "hivemetastore"
+ - "hiveserver"
+ - "hive-metastore-postgresql"
+ - "namenode"
+ volumes:
+ -
${TRINO_PLUGIN_DIR:-${HUDI_WS}/docker/trino/empty-overlay}:/opt/hudi-plugin-overlay:ro
+ - ${HUDI_WS}:/var/hoodie/ws
+
volumes:
namenode:
historyserver:
diff --git
a/docker/compose/docker-compose_hadoop340_hive2310_spark402_arm64.yml
b/docker/compose/docker-compose_hadoop340_hive2310_spark402_arm64.yml
index edc1a36bd28a..adf964e2288b 100644
--- a/docker/compose/docker-compose_hadoop340_hive2310_spark402_arm64.yml
+++ b/docker/compose/docker-compose_hadoop340_hive2310_spark402_arm64.yml
@@ -78,7 +78,7 @@ services:
volumes:
- historyserver:/hadoop/yarn/timeline
- # Pure Hive 2.3.10 stack (postgres 2.3 schema -> HMS 2.3.10 → HS2 2.3.10).
+ # Pure Hive 2.3.10 stack (postgres 2.3 schema -> HMS 2.3.10 -> HS2 2.3.10).
# Matches hudi-spark-bundle's compile-time Hive 2.3 client, so Hudi hive-sync
# talks to HMS natively (no Thrift get_table incompat, no sharedPrefixes
hack).
# Hadoop 3.4.0 HDFS is backward-compat for the 2.8.4-based Hive client.
@@ -256,6 +256,29 @@ services:
depends_on:
- minio
+ # Gated behind the "trino" compose profile: inert for the default hive-sync
CI
+ # rows, only starts when COMPOSE_PROFILES=trino. The plugin overlay defaults
to
+ # docker/trino/empty-overlay (baked-in plugin used); set TRINO_PLUGIN_DIR to
a
+ # locally-built trino-hudi plugin dir to override it at container start.
+ trinocoordinator:
+ image: apachehudi/hudi-trino_481:latest
+ profiles: ["trino"]
+ hostname: trinocoordinator
+ container_name: trinocoordinator
+ ports:
+ - "8092:8080"
+ depends_on:
+ - "hivemetastore"
+ - "namenode"
+ links:
+ - "hivemetastore"
+ - "hiveserver"
+ - "hive-metastore-postgresql"
+ - "namenode"
+ volumes:
+ -
${TRINO_PLUGIN_DIR:-${HUDI_WS}/docker/trino/empty-overlay}:/opt/hudi-plugin-overlay:ro
+ - ${HUDI_WS}:/var/hoodie/ws
+
volumes:
namenode:
historyserver:
diff --git a/docker/demo/sparksql-stock-ticks-trino.commands
b/docker/demo/sparksql-stock-ticks-trino.commands
new file mode 100644
index 000000000000..e90d591db617
--- /dev/null
+++ b/docker/demo/sparksql-stock-ticks-trino.commands
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Self-contained COW + MOR stock-ticks seed for the Trino E2E tests
+// (ITTestTrinoStockTicks). Mirrors the retired trino-batch1.commands data
+// shape without the Kafka/streaming pipeline, which integ2 does not exercise.
+// ts stays STRING so Trino's CSV_UNQUOTED output matches the test's exact
+// row assertion: GOOG,2018-08-31 10:29:00,6330,1230.5,1230.5
+spark.sql("""
+ CREATE TABLE stock_ticks_cow (
+ symbol STRING,
+ ts STRING,
+ volume LONG,
+ open DOUBLE,
+ close DOUBLE,
+ dt STRING
+ ) USING hudi
+ PARTITIONED BY (dt)
+ LOCATION '/user/hive/warehouse/stock_ticks_cow'
+ TBLPROPERTIES (
+ 'primaryKey' = 'symbol',
+ 'preCombineField' = 'ts',
+ 'hoodie.datasource.hive_sync.enable' = 'true',
+ 'hoodie.datasource.hive_sync.database' = 'default',
+ 'hoodie.datasource.hive_sync.table' = 'stock_ticks_cow',
+ 'hoodie.datasource.hive_sync.jdbcurl' = 'jdbc:hive2://hiveserver:10000/',
+ 'hoodie.datasource.hive_sync.mode' = 'jdbc',
+ 'hoodie.datasource.hive_sync.partition_fields' = 'dt',
+ 'hoodie.datasource.hive_sync.partition_extractor_class' =
'org.apache.hudi.hive.MultiPartKeysValueExtractor',
+ 'hoodie.datasource.hive_sync.username' = 'hive',
+ 'hoodie.datasource.hive_sync.password' = 'hive'
+ )
+""")
+
+spark.sql("INSERT INTO stock_ticks_cow VALUES ('GOOG', '2018-08-31 10:29:00',
6330, 1230.5, 1230.5, '2018-08-31')")
+spark.sql("select symbol, ts, volume, open, close from
stock_ticks_cow").show(10, false)
+println("STOCK_TICKS_COW_SETUP_SUCCESS")
+
+// MOR variant: identical schema and seed row. 'type' = 'mor' makes hive sync
+// register stock_ticks_mor_ro / stock_ticks_mor_rt; the initial insert writes
+// parquet base files, and the follow-up UPDATE below adds a log-only delta so
+// the _ro and _rt views actually diverge.
+spark.sql("""
+ CREATE TABLE stock_ticks_mor (
+ symbol STRING,
+ ts STRING,
+ volume LONG,
+ open DOUBLE,
+ close DOUBLE,
+ dt STRING
+ ) USING hudi
+ PARTITIONED BY (dt)
+ LOCATION '/user/hive/warehouse/stock_ticks_mor'
+ TBLPROPERTIES (
+ 'type' = 'mor',
+ 'primaryKey' = 'symbol',
+ 'preCombineField' = 'ts',
+ 'hoodie.datasource.hive_sync.enable' = 'true',
+ 'hoodie.datasource.hive_sync.database' = 'default',
+ 'hoodie.datasource.hive_sync.table' = 'stock_ticks_mor',
+ 'hoodie.datasource.hive_sync.jdbcurl' = 'jdbc:hive2://hiveserver:10000/',
+ 'hoodie.datasource.hive_sync.mode' = 'jdbc',
+ 'hoodie.datasource.hive_sync.partition_fields' = 'dt',
+ 'hoodie.datasource.hive_sync.partition_extractor_class' =
'org.apache.hudi.hive.MultiPartKeysValueExtractor',
+ 'hoodie.datasource.hive_sync.username' = 'hive',
+ 'hoodie.datasource.hive_sync.password' = 'hive'
+ )
+""")
+
+spark.sql("INSERT INTO stock_ticks_mor VALUES ('GOOG', '2018-08-31 10:29:00',
6330, 1230.5, 1230.5, '2018-08-31')")
+
+// Log-only delta on the same key: UPDATE routes through upsert, so the
existing
+// file group gains a log file that _ro must ignore (base row: 10:29:00) and
+// _rt must merge (10:59:00). One delta commit stays far below the inline
+// compaction threshold, so the log survives for the read-path split to matter.
+// open/close use .25/.5 so the double renders exactly in Trino's CSV output.
+spark.sql("UPDATE stock_ticks_mor SET ts = '2018-08-31 10:59:00', volume =
9021, open = 1227.25, close = 1227.5 WHERE symbol = 'GOOG'")
+spark.sql("select symbol, ts, volume, open, close from
stock_ticks_mor").show(10, false)
+println("STOCK_TICKS_MOR_SETUP_SUCCESS")
+
+// Debug aid: proves stock_ticks_mor_ro / stock_ticks_mor_rt got registered.
+spark.sql("show tables").show(100, false)
+println("STOCK_TICKS_TRINO_SETUP_SUCCESS")
diff --git a/docker/demo/trino-batch1.commands
b/docker/demo/trino-batch1.commands
deleted file mode 100644
index d89c19b0bf0b..000000000000
--- a/docker/demo/trino-batch1.commands
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-select symbol, max(ts) from stock_ticks_cow group by symbol HAVING symbol =
'GOOG';
-select symbol, max(ts) from stock_ticks_mor_ro group by symbol HAVING symbol =
'GOOG';
-select symbol, ts, volume, open, close from stock_ticks_cow where symbol =
'GOOG';
-select symbol, ts, volume, open, close from stock_ticks_mor_ro where symbol
= 'GOOG';
diff --git a/docker/demo/trino-batch2-after-compaction.commands
b/docker/demo/trino-batch2-after-compaction.commands
deleted file mode 100644
index da42b4728252..000000000000
--- a/docker/demo/trino-batch2-after-compaction.commands
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-select symbol, max(ts) from stock_ticks_mor_ro group by symbol HAVING symbol =
'GOOG';
-select symbol, ts, volume, open, close from stock_ticks_mor_ro where symbol
= 'GOOG';
diff --git a/docker/demo/trino-table-check.commands
b/docker/demo/trino-table-check.commands
deleted file mode 100644
index 4362d79fe770..000000000000
--- a/docker/demo/trino-table-check.commands
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-show tables;
diff --git a/docker/trino/.dockerignore b/docker/trino/.dockerignore
new file mode 100644
index 000000000000..3471b8973e1f
--- /dev/null
+++ b/docker/trino/.dockerignore
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Keep the shim build tree out of the docker build context; build_image.sh
+# stages the one plugin dir the Dockerfile needs into plugin/.
+shim/
diff --git a/docker/trino/.gitignore b/docker/trino/.gitignore
new file mode 100644
index 000000000000..939686302a85
--- /dev/null
+++ b/docker/trino/.gitignore
@@ -0,0 +1,3 @@
+# Transient staging dir populated by build_image.sh (leading slash: must not
+# swallow the shim's io/trino/plugin/ source package under shim/).
+/plugin/
diff --git a/docker/trino/Dockerfile b/docker/trino/Dockerfile
new file mode 100644
index 000000000000..3189368a2283
--- /dev/null
+++ b/docker/trino/Dockerfile
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+ARG TRINO_VERSION=481
+FROM trinodb/trino:${TRINO_VERSION}
+
+USER root
+
+# Replace the bundled hudi plugin with the locally-built trino-hudi plugin
+# staged into the build context at plugin/ (see build_image.sh).
+#
+# The staged plugin dir carries only the connector's top-level runtime jars.
+# fs.hadoop.enabled=true additionally needs the isolated HDFS loader jar set at
+# <plugin>/hdfs (io.trino.filesystem.manager.HdfsFileSystemLoader). That jar
set
+# is only distributed inside the trino-server tarball / base image
+# (io.trino:trino-hdfs:zip is not on Maven Central), so preserve the stock hudi
+# plugin's version-matched copy before replacing it, and re-attach it when the
+# staged plugin dir lacks one. /opt/hudi-hdfs-lib stays in the image so the
+# overlay entrypoint can do the same for bind-mounted plugin overlays.
+RUN cp -r /usr/lib/trino/plugin/hudi/hdfs /opt/hudi-hdfs-lib \
+ && rm -rf /usr/lib/trino/plugin/hudi
+COPY --chown=trino:trino plugin/ /usr/lib/trino/plugin/hudi/
+RUN if [ ! -d /usr/lib/trino/plugin/hudi/hdfs ]; then \
+ cp -r /opt/hudi-hdfs-lib /usr/lib/trino/plugin/hudi/hdfs; \
+ fi \
+ && chown -R trino:trino /usr/lib/trino/plugin/hudi /opt/hudi-hdfs-lib
+
+# Bake the Hudi E2E Trino config (coordinator, catalog, hadoop-conf) into the
image.
+COPY --chown=trino:trino etc/ /etc/trino/
+
+# Overlay-aware entrypoint: a bind-mounted plugin overlay (if present) replaces
+# the baked-in plugin at container start, otherwise the baked-in plugin is
used.
+COPY --chown=trino:trino overlay-entrypoint.sh /opt/overlay-entrypoint.sh
+RUN chmod +x /opt/overlay-entrypoint.sh
+
+USER trino
+ENTRYPOINT ["/opt/overlay-entrypoint.sh"]
diff --git a/docker/trino/build_image.sh b/docker/trino/build_image.sh
new file mode 100755
index 000000000000..da1cc8fe2ef1
--- /dev/null
+++ b/docker/trino/build_image.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Builds the apachehudi/hudi-trino_<version> image with a locally-built
+# trino-hudi plugin baked in. The plugin dir (typically the in-repo shim's
+# docker/trino/shim/target/trino-hudi-<v>, see docker/trino/shim/pom.xml) is
+# staged into the build context at docker/trino/plugin/ (gitignored), then
+# baked into the image.
+# Usage: ./build_image.sh --plugin-dir <path> [--trino-version <v>]
[--image-tag <t>]
+# Typical: ./build_image.sh --plugin-dir "$(dirname
"$0")/shim/target/trino-hudi-481"
+# Note: --trino-version must match the shim pom's parent version and the root
+# pom's trino.version property.
+
+set -e
+
+# Default values
+PLUGIN_DIR=""
+TRINO_VERSION="481"
+IMAGE_TAG="latest"
+
+# Parse command-line arguments
+while [[ "$#" -gt 0 ]]; do
+ case $1 in
+ --plugin-dir) PLUGIN_DIR="$2"; shift ;;
+ --trino-version) TRINO_VERSION="$2"; shift ;;
+ --image-tag) IMAGE_TAG="$2"; shift ;;
+ *) echo "Unknown parameter passed: $1"; exit 1 ;;
+ esac
+ shift
+done
+
+# Directory of this script, so the build context path is stable regardless of
cwd
+SCRIPT_DIR=$(cd $(dirname "$0") && pwd)
+
+# Validate --plugin-dir: required, must exist and be non-empty
+if [ -z "$PLUGIN_DIR" ]; then
+ echo "Error: --plugin-dir <path> is required (the locally-built trino-hudi
plugin directory)." >&2
+ exit 1
+fi
+if [ ! -d "$PLUGIN_DIR" ]; then
+ echo "Error: plugin dir '$PLUGIN_DIR' does not exist." >&2
+ exit 1
+fi
+if [ -z "$(ls -A "$PLUGIN_DIR" 2>/dev/null)" ]; then
+ echo "Error: plugin dir '$PLUGIN_DIR' is empty." >&2
+ exit 1
+fi
+
+# Stage the plugin into the build context (plugin/ must be IN the context to
be COPY-able)
+STAGE_DIR="$SCRIPT_DIR/plugin"
+echo "Staging plugin from '$PLUGIN_DIR' into '$STAGE_DIR'"
+rm -rf "$STAGE_DIR"
+cp -r "$PLUGIN_DIR" "$STAGE_DIR"
+
+IMAGE="apachehudi/hudi-trino_${TRINO_VERSION}:${IMAGE_TAG}"
+echo "Building $IMAGE (TRINO_VERSION=${TRINO_VERSION})"
+docker build --build-arg TRINO_VERSION="${TRINO_VERSION}" -t "$IMAGE"
"$SCRIPT_DIR"
+
+# Clean up the staged plugin dir
+echo "Cleaning up staged plugin dir '$STAGE_DIR'"
+rm -rf "$STAGE_DIR"
+
+echo "Done: $IMAGE"
diff --git a/docker/trino/empty-overlay/.gitkeep
b/docker/trino/empty-overlay/.gitkeep
new file mode 100644
index 000000000000..9e386d0cdd88
--- /dev/null
+++ b/docker/trino/empty-overlay/.gitkeep
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# This file exists only to keep the directory in git: it is the compose
+# default (empty) mount source for the trino-hudi plugin overlay, and the
+# overlay entrypoint applies an overlay only when it contains jars.
diff --git a/docker/trino/etc/catalog/hudi.properties
b/docker/trino/etc/catalog/hudi.properties
new file mode 100644
index 000000000000..14c17746a897
--- /dev/null
+++ b/docker/trino/etc/catalog/hudi.properties
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Native trino-hudi connector (assembled from org.apache.hudi:hudi-trino by the
+# docker/trino/shim project).
+# Pre-Trino 398 the only way to read Hudi was the hive-connector +
hudi-trino-bundle shim;
+# plugin/trino-hudi landed upstream at 398 as the native replacement, hence
+# connector.name=hudi.
+connector.name=hudi
+hive.metastore=thrift
+hive.metastore.uri=thrift://hivemetastore:9083
+# trino-filesystem-manager flag that turns on the legacy Hadoop FileSystem path
+# (HDFS via fs.defaultFS in hive.config.resources). Without this the plugin
can't
+# read hdfs:// URIs in Trino 460+.
+fs.hadoop.enabled=true
+hive.config.resources=/etc/trino/hadoop-conf/core-site.xml,/etc/trino/hadoop-conf/hdfs-site.xml
diff --git a/docker/trino/etc/config.properties
b/docker/trino/etc/config.properties
new file mode 100644
index 000000000000..8239eacffdf6
--- /dev/null
+++ b/docker/trino/etc/config.properties
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Single-node Trino: the coordinator also runs splits. Good enough for E2E,
+# halves container startup vs a separate worker.
+coordinator=true
+node-scheduler.include-coordinator=true
+http-server.http.port=8080
+discovery.uri=http://trinocoordinator:8080
+query.max-memory=2GB
+query.max-memory-per-node=1GB
diff --git a/docker/trino/etc/hadoop-conf/core-site.xml
b/docker/trino/etc/hadoop-conf/core-site.xml
new file mode 100644
index 000000000000..455fbb9181d6
--- /dev/null
+++ b/docker/trino/etc/hadoop-conf/core-site.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <property>
+ <name>fs.defaultFS</name>
+ <value>hdfs://namenode:8020</value>
+ </property>
+</configuration>
diff --git a/docker/trino/etc/hadoop-conf/hdfs-site.xml
b/docker/trino/etc/hadoop-conf/hdfs-site.xml
new file mode 100644
index 000000000000..5bd3bf51dffe
--- /dev/null
+++ b/docker/trino/etc/hadoop-conf/hdfs-site.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <property>
+ <name>dfs.client.use.datanode.hostname</name>
+ <value>true</value>
+ </property>
+ <property>
+ <name>dfs.replication</name>
+ <value>1</value>
+ </property>
+</configuration>
diff --git a/docker/trino/etc/jvm.config b/docker/trino/etc/jvm.config
new file mode 100644
index 000000000000..b1d8ff3772dc
--- /dev/null
+++ b/docker/trino/etc/jvm.config
@@ -0,0 +1,38 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+-server
+-Xmx2G
+-XX:InitialRAMPercentage=80
+-XX:MaxRAMPercentage=80
+-XX:G1HeapRegionSize=32M
+-XX:+ExplicitGCInvokesConcurrent
+-XX:+ExitOnOutOfMemoryError
+-XX:+HeapDumpOnOutOfMemoryError
+-XX:-OmitStackTraceInFastThrow
+-XX:ReservedCodeCacheSize=512M
+-XX:PerMethodRecompilationCutoff=10000
+-XX:PerBytecodeRecompilationCutoff=10000
+-Djdk.attach.allowAttachSelf=true
+-Djdk.nio.maxCachedBufferSize=2000000
+-Dfile.encoding=UTF-8
+# Allow loading dynamic agents (used by JOL, referenced by Trino's runtime).
+-XX:+EnableDynamicAgentLoading
+# NOTE: do NOT add -XX:GCLockerRetryAllocationCount here (Hudi's JDK 11/17 CI
+# workaround): the GCLocker was removed in modern JDKs and the
trinodb/trino:481
+# JVM (JDK 25) refuses to start on the unrecognized option.
diff --git a/docker/trino/etc/node.properties b/docker/trino/etc/node.properties
new file mode 100644
index 000000000000..7e0222cc3aec
--- /dev/null
+++ b/docker/trino/etc/node.properties
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+node.environment=hudi
+# Fixed node.id so a container restart reuses the same identity in the
discovery service.
+node.id=hudi-trino-coordinator
+node.data-dir=/data/trino
diff --git a/docker/trino/overlay-entrypoint.sh
b/docker/trino/overlay-entrypoint.sh
new file mode 100755
index 000000000000..2ba9b517e26a
--- /dev/null
+++ b/docker/trino/overlay-entrypoint.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Overlay-aware Trino entrypoint. If a plugin overlay is bind-mounted at
+# /opt/hudi-plugin-overlay (set TRINO_PLUGIN_DIR to the in-repo shim's
+# docker/trino/shim/target/trino-hudi-<ver> build output, or to a trinodb/trino
+# checkout's plugin/trino-hudi/target/trino-hudi-<ver>), fully replace the
+# image's baked-in trino-hudi plugin with it (rm -rf then copy), so plugin
+# iterations need only a rebuild of that dir plus a container restart, not a
+# docker image rebuild. Otherwise the image-baked plugin is used as-is.
+set -euo pipefail
+
+OVERLAY=/opt/hudi-plugin-overlay
+PLUGIN_DIR=/usr/lib/trino/plugin/hudi
+
+# The overlay counts as present only if it holds at least one jar: the compose
+# default mount is docker/trino/empty-overlay, whose .gitkeep must not trigger
+# a wipe of the baked-in plugin.
+if [ -d "$OVERLAY" ] && [ -n "$(find "$OVERLAY" -name '*.jar' -print -quit
2>/dev/null)" ]; then
+ echo "Applying trino-hudi plugin overlay from $OVERLAY (fully replacing
$PLUGIN_DIR)"
+ rm -rf "$PLUGIN_DIR"
+ mkdir -p "$PLUGIN_DIR"
+ cp -r "$OVERLAY"/. "$PLUGIN_DIR"/
+else
+ echo "No plugin overlay found at $OVERLAY; using the image-baked trino-hudi
plugin as-is."
+fi
+
+# Overlays built from the in-repo shim
(docker/trino/shim/target/trino-hudi-<ver>)
+# lack the hdfs/ loader dir that fs.hadoop.enabled=true needs; restore the copy
+# the image preserved from the stock plugin (see Dockerfile).
+if [ ! -d "$PLUGIN_DIR/hdfs" ] && [ -d /opt/hudi-hdfs-lib ]; then
+ echo "Restoring hdfs/ loader dir into $PLUGIN_DIR from /opt/hudi-hdfs-lib"
+ cp -r /opt/hudi-hdfs-lib "$PLUGIN_DIR/hdfs"
+fi
+
+exec /usr/lib/trino/bin/run-trino
diff --git a/docker/trino/shim/pom.xml b/docker/trino/shim/pom.xml
new file mode 100644
index 000000000000..c263b0d43b21
--- /dev/null
+++ b/docker/trino/shim/pom.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ In-repo mirror of the RFC-105 Trino-side shim (trinodb/trino
plugin/trino-hudi),
+ which is not yet released upstream. It exists so this repo can assemble the
+ deployable Trino plugin directory (hudi-trino jar + runtime deps + service
+ descriptor) for the E2E docker image without a trinodb/trino checkout, and it
+ stays useful afterwards: CI must test hudi-trino at HEAD while the upstream
shim
+ pins released versions.
+
+ Standalone project - NOT part of the Hudi reactor and never deployed.
+
+ Build (JDK 25, after installing hudi-trino into the local m2):
+ mvn -f docker/trino/shim/pom.xml clean package -DskipTests
+ Output plugin dir: target/trino-hudi-<trino.version>/
+ Use `package`; `install` is hard-disabled via maven.install.skip below
because
+ installing would shadow the real io.trino:trino-hudi release coordinates in
+ the local repository.
+
+ The parent version below must stay in sync with trino.version in the root
pom,
+ the TRINO_VERSION ARG in docker/trino/Dockerfile, and the compose image tag.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>io.trino</groupId>
+ <artifactId>trino-root</artifactId>
+ <version>481</version>
+ <!-- Resolve the parent from Maven Central, never the file system. -->
+ <relativePath />
+ </parent>
+
+ <artifactId>trino-hudi</artifactId>
+ <packaging>trino-plugin</packaging>
+ <description>Trino - Hudi connector plugin assembly (in-repo E2E shim
mirroring the upstream plugin/trino-hudi shim planned by RFC-105; never
deployed)</description>
+
+ <properties>
+ <!-- Hudi connector under test; override with -Ddep.hudi.version=...
-->
+ <dep.hudi.version>1.3.0-SNAPSHOT</dep.hudi.version>
+ <!-- Assembly-only project: skip airbase/basepom checks (license header
+ style, checkstyle, enforcer, modernizer, duplicate-finder,
dependency
+ analysis). ASF headers are enforced separately by
+ scripts/release/validate_source_copyright.sh. -->
+ <air.check.skip-all>true</air.check.skip-all>
+ <!-- Test infra only: never deploy/release this artifact, and never
+ install it - a stray install would publish a stub at the real
+ io.trino:trino-hudi coordinates and poison the local m2. -->
+ <maven.deploy.skip>true</maven.deploy.skip>
+ <maven.install.skip>true</maven.install.skip>
+ <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
+ </properties>
+
+ <dependencies>
+ <!-- HudiPlugin.java uses ImmutableList. -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.hudi</groupId>
+ <artifactId>hudi-trino</artifactId>
+ <version>${dep.hudi.version}</version>
+ <exclusions>
+ <!-- Defense in depth, mirroring the upstream shim: the
hudi-trino
+ pom should already keep these out, but explicit exclusions
+ survive upstream churn. orc-core ships a stale relocated
copy
+ of io.airlift.slice.XxHash64 that would shadow slice at
+ runtime; Trino uses its own ORC reader. -->
+ <exclusion>
+ <groupId>org.apache.arrow</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.hudi</groupId>
+ <artifactId>hudi-timeline-service</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.orc</groupId>
+ <artifactId>orc-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.lance</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.rocksdb</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- Trino SPI surface: provided by the server at plugin load; must
stay
+ out of the assembled plugin directory. Direct provided-scope
+ declarations override any compile-scope transitive arrivals so the
+ trino-maven-plugin SPI dependency check passes. -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.airlift</groupId>
+ <artifactId>slice</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-api-incubator</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.opentelemetry</groupId>
+ <artifactId>opentelemetry-context</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.trino</groupId>
+ <artifactId>trino-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- NOTE: unlike the upstream 482-SNAPSHOT shim, jts-core must NOT be
+ declared provided here: it is not part of the Trino 481 SPI
surface
+ and SpiDependencyChecker rejects provided scope for it. It ships
+ inside the plugin dir at its transitive scope instead. -->
+ </dependencies>
+</project>
diff --git
a/docker/trino/shim/src/main/java/io/trino/plugin/hudi/HudiPlugin.java
b/docker/trino/shim/src/main/java/io/trino/plugin/hudi/HudiPlugin.java
new file mode 100644
index 000000000000..5c3f185ec6ab
--- /dev/null
+++ b/docker/trino/shim/src/main/java/io/trino/plugin/hudi/HudiPlugin.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.trino.plugin.hudi;
+
+import com.google.common.collect.ImmutableList;
+import io.trino.spi.Plugin;
+import io.trino.spi.connector.ConnectorFactory;
+
+/**
+ * Thin shim plugin mirroring the upstream trinodb/trino plugin/trino-hudi
module
+ * (RFC-105). Same FQCN as the copy inside the hudi-trino jar - the
duplication is
+ * intentional: trino-maven-plugin's service descriptor generator only scans
this
+ * module's own classes, and both class bodies are identical, so classloader
+ * ordering does not matter.
+ */
+public class HudiPlugin
+ implements Plugin
+{
+ @Override
+ public Iterable<ConnectorFactory> getConnectorFactories()
+ {
+ return ImmutableList.of(new HudiConnectorFactory());
+ }
+}
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestBase.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestBase.java
index 60493f98931d..eba2270a430d 100644
--- a/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestBase.java
+++ b/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestBase.java
@@ -62,7 +62,6 @@ public abstract class ITTestBase {
protected static final String ADHOC_2_CONTAINER = "/adhoc-2";
protected static final String HIVESERVER = "/hiveserver";
protected static final String PRESTO_COORDINATOR = "/presto-coordinator-1";
- protected static final String TRINO_COORDINATOR = "/trino-coordinator-1";
protected static final String HOODIE_WS_ROOT = "/var/hoodie/ws";
protected static final String HOODIE_JAVA_APP = HOODIE_WS_ROOT +
"/hudi-spark-datasource/hudi-spark/run_hoodie_app.sh";
protected static final String HOODIE_GENERATE_APP = HOODIE_WS_ROOT +
"/hudi-spark-datasource/hudi-spark/run_hoodie_generate_app.sh";
@@ -77,7 +76,6 @@ public abstract class ITTestBase {
HOODIE_WS_ROOT +
"/docker/hoodie/hadoop/hive_base/target/hoodie-utilities.jar";
protected static final String HIVE_SERVER_JDBC_URL =
"jdbc:hive2://hiveserver:10000";
protected static final String PRESTO_COORDINATOR_URL =
"presto-coordinator-1:8090";
- protected static final String TRINO_COORDINATOR_URL =
"trino-coordinator-1:8091";
protected static final String HADOOP_CONF_DIR = "/etc/hadoop";
// Skip these lines when capturing output from hive
@@ -126,12 +124,6 @@ public abstract class ITTestBase {
.append(" -f " + commandFile).toString();
}
- static String getTrinoConsoleCommand(String commandFile) {
- return new StringBuilder().append("trino --server " +
TRINO_COORDINATOR_URL)
- .append(" --catalog hive --schema default")
- .append(" -f " + commandFile).toString();
- }
-
@BeforeEach
public void init() {
String dockerHost = (OVERRIDDEN_DOCKER_HOST != null) ?
OVERRIDDEN_DOCKER_HOST : DEFAULT_DOCKER_HOST;
@@ -320,20 +312,6 @@ public abstract class ITTestBase {
.exec();
}
- Pair<String, String> executeTrinoCommandFile(String commandFile) throws
Exception {
- String trinoCmd = getTrinoConsoleCommand(commandFile);
- TestExecStartResultCallback callback =
executeCommandStringInDocker(ADHOC_1_CONTAINER, trinoCmd, true);
- return Pair.of(callback.getStdout().toString().trim(),
callback.getStderr().toString().trim());
- }
-
- void executeTrinoCopyCommand(String fromFile, String remotePath) {
- Container adhocContainer = runningContainers.get(ADHOC_1_CONTAINER);
- dockerClient.copyArchiveToContainerCmd(adhocContainer.getId())
- .withHostResource(fromFile)
- .withRemotePath(remotePath)
- .exec();
- }
-
private void saveUpLogs() {
try {
// save up the Hive log files for introspection
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestHoodieDemo.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestHoodieDemo.java
index d9d2c20dc2bb..d9cc3e526f7f 100644
--- a/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestHoodieDemo.java
+++ b/hudi-integ-test/src/test/java/org/apache/hudi/integ/ITTestHoodieDemo.java
@@ -38,28 +38,18 @@ import java.util.List;
*/
public class ITTestHoodieDemo extends ITTestBase {
- private static final String TRINO_TABLE_CHECK_FILENAME =
"trino-table-check.commands";
- private static final String TRINO_BATCH1_FILENAME = "trino-batch1.commands";
- private static final String TRINO_BATCH2_FILENAME =
"trino-batch2-after-compaction.commands";
-
private static final String HDFS_DATA_DIR = "/usr/hive/data/input";
private static final String HDFS_BATCH_PATH1 = HDFS_DATA_DIR +
"/batch_1.json";
private static final String HDFS_BATCH_PATH2 = HDFS_DATA_DIR +
"/batch_2.json";
private static final String HDFS_PRESTO_INPUT_TABLE_CHECK_PATH =
HDFS_DATA_DIR + "/presto-table-check.commands";
private static final String HDFS_PRESTO_INPUT_BATCH1_PATH = HDFS_DATA_DIR +
"/presto-batch1.commands";
private static final String HDFS_PRESTO_INPUT_BATCH2_PATH = HDFS_DATA_DIR +
"/presto-batch2-after-compaction.commands";
- private static final String HDFS_TRINO_INPUT_TABLE_CHECK_PATH =
HDFS_DATA_DIR + "/" + TRINO_TABLE_CHECK_FILENAME;
- private static final String HDFS_TRINO_INPUT_BATCH1_PATH = HDFS_DATA_DIR +
"/" + TRINO_BATCH1_FILENAME;
- private static final String HDFS_TRINO_INPUT_BATCH2_PATH = HDFS_DATA_DIR +
"/" + TRINO_BATCH2_FILENAME;
private static final String INPUT_BATCH_PATH1 = HOODIE_WS_ROOT +
"/docker/demo/data/batch_1.json";
private static final String PRESTO_INPUT_TABLE_CHECK_RELATIVE_PATH =
"/docker/demo/presto-table-check.commands";
private static final String PRESTO_INPUT_BATCH1_RELATIVE_PATH =
"/docker/demo/presto-batch1.commands";
private static final String INPUT_BATCH_PATH2 = HOODIE_WS_ROOT +
"/docker/demo/data/batch_2.json";
private static final String PRESTO_INPUT_BATCH2_RELATIVE_PATH =
"/docker/demo/presto-batch2-after-compaction.commands";
- private static final String TRINO_INPUT_TABLE_CHECK_RELATIVE_PATH =
"/docker/demo/" + TRINO_TABLE_CHECK_FILENAME;
- private static final String TRINO_INPUT_BATCH1_RELATIVE_PATH =
"/docker/demo/" + TRINO_BATCH1_FILENAME;
- private static final String TRINO_INPUT_BATCH2_RELATIVE_PATH =
"/docker/demo/" + TRINO_BATCH2_FILENAME;
private static final String COW_BASE_PATH =
"/user/hive/warehouse/stock_ticks_cow";
private static final String MOR_BASE_PATH =
"/user/hive/warehouse/stock_ticks_mor";
@@ -120,16 +110,15 @@ public class ITTestHoodieDemo extends ITTestBase {
// batch 1
ingestFirstBatchAndHiveSync();
testHiveAfterFirstBatch();
- // TODO(HUDI-8269, HUDI-8270): fix integration tests with Presto and Trino
+ // TODO(HUDI-8269): fix integration tests with Presto. The legacy Trino
demo
+ // path was retired in favor of the integ2 testcontainers Trino E2E suite.
// testPrestoAfterFirstBatch();
- // testTrinoAfterFirstBatch();
testSparkSQLAfterFirstBatch();
// batch 2
ingestSecondBatchAndHiveSync();
testHiveAfterSecondBatch();
// testPrestoAfterSecondBatch();
- // testTrinoAfterSecondBatch();
testSparkSQLAfterSecondBatch();
// TODO: HUDI-8572
// testIncrementalHiveQueryBeforeCompaction();
@@ -141,7 +130,6 @@ public class ITTestHoodieDemo extends ITTestBase {
testIncrementalSparkSQLQuery();
testHiveAfterSecondBatchAfterCompaction();
// testPrestoAfterSecondBatchAfterCompaction();
- // testTrinoAfterSecondBatchAfterCompaction();
// TODO: HUDI-8572
// testIncrementalHiveQueryAfterCompaction();
}
@@ -159,14 +147,12 @@ public class ITTestHoodieDemo extends ITTestBase {
ingestFirstBatchAndHiveSync();
testHiveAfterFirstBatch();
//testPrestoAfterFirstBatch();
- //testTrinoAfterFirstBatch();
//testSparkSQLAfterFirstBatch();
// batch 2
ingestSecondBatchAndHiveSync();
testHiveAfterSecondBatch();
//testPrestoAfterSecondBatch();
- //testTrinoAfterSecondBatch();
//testSparkSQLAfterSecondBatch();
testIncrementalHiveQueryBeforeCompaction();
//testIncrementalSparkSQLQuery();
@@ -175,7 +161,6 @@ public class ITTestHoodieDemo extends ITTestBase {
scheduleAndRunCompaction();
testHiveAfterSecondBatchAfterCompaction();
//testPrestoAfterSecondBatchAfterCompaction();
- //testTrinoAfterSecondBatchAfterCompaction();
//testIncrementalHiveQueryAfterCompaction();
}
@@ -196,10 +181,6 @@ public class ITTestHoodieDemo extends ITTestBase {
executePrestoCopyCommand(System.getProperty("user.dir") + "/.." +
PRESTO_INPUT_TABLE_CHECK_RELATIVE_PATH, HDFS_DATA_DIR);
executePrestoCopyCommand(System.getProperty("user.dir") + "/.." +
PRESTO_INPUT_BATCH1_RELATIVE_PATH, HDFS_DATA_DIR);
executePrestoCopyCommand(System.getProperty("user.dir") + "/.." +
PRESTO_INPUT_BATCH2_RELATIVE_PATH, HDFS_DATA_DIR);
-
- executeTrinoCopyCommand(System.getProperty("user.dir") + "/.." +
TRINO_INPUT_TABLE_CHECK_RELATIVE_PATH, HDFS_DATA_DIR);
- executeTrinoCopyCommand(System.getProperty("user.dir") + "/.." +
TRINO_INPUT_BATCH1_RELATIVE_PATH, HDFS_DATA_DIR);
- executeTrinoCopyCommand(System.getProperty("user.dir") + "/.." +
TRINO_INPUT_BATCH2_RELATIVE_PATH, HDFS_DATA_DIR);
}
private void ingestFirstBatchAndHiveSync() throws Exception {
@@ -359,20 +340,6 @@ public class ITTestHoodieDemo extends ITTestBase {
"\"GOOG\",\"2018-08-31
10:29:00\",\"3391\",\"1230.1899\",\"1230.085\"", 2);
}
- private void testTrinoAfterFirstBatch() throws Exception {
- Pair<String, String> stdOutErrPair =
executeTrinoCommandFile(HDFS_TRINO_INPUT_TABLE_CHECK_PATH);
- assertStdOutContains(stdOutErrPair, "stock_ticks_cow", 2);
- assertStdOutContains(stdOutErrPair, "stock_ticks_mor", 6);
-
- stdOutErrPair = executeTrinoCommandFile(HDFS_TRINO_INPUT_BATCH1_PATH);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 10:29:00\"", 4);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 09:59:00\",\"6330\",\"1230.5\",\"1230.02\"", 2);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31
10:29:00\",\"3391\",\"1230.1899\",\"1230.085\"", 2);
- }
-
private void testHiveAfterSecondBatch() throws Exception {
Pair<String, String> stdOutErrPair =
executeHiveCommandFile(HIVE_BATCH1_COMMANDS);
assertStdOutContains(stdOutErrPair, "| symbol | _c1
|\n+---------+----------------------+\n"
@@ -406,20 +373,6 @@ public class ITTestHoodieDemo extends ITTestBase {
"\"GOOG\",\"2018-08-31
10:59:00\",\"9021\",\"1227.1993\",\"1227.215\"");
}
- private void testTrinoAfterSecondBatch() throws Exception {
- Pair<String, String> stdOutErrPair =
executeTrinoCommandFile(HDFS_TRINO_INPUT_BATCH1_PATH);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 10:29:00\"", 2);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 10:59:00\"", 2);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 09:59:00\",\"6330\",\"1230.5\",\"1230.02\"", 2);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31
10:29:00\",\"3391\",\"1230.1899\",\"1230.085\"");
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31
10:59:00\",\"9021\",\"1227.1993\",\"1227.215\"");
- }
-
private void testHiveAfterSecondBatchAfterCompaction() throws Exception {
Pair<String, String> stdOutErrPair =
executeHiveCommandFile(HIVE_BATCH2_COMMANDS);
assertStdOutContains(stdOutErrPair, "| symbol | _c1
|\n+---------+----------------------+\n"
@@ -442,16 +395,6 @@ public class ITTestHoodieDemo extends ITTestBase {
"\"GOOG\",\"2018-08-31
10:59:00\",\"9021\",\"1227.1993\",\"1227.215\"");
}
- private void testTrinoAfterSecondBatchAfterCompaction() throws Exception {
- Pair<String, String> stdOutErrPair =
executeTrinoCommandFile(HDFS_TRINO_INPUT_BATCH2_PATH);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 10:59:00\"", 2);
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31 09:59:00\",\"6330\",\"1230.5\",\"1230.02\"");
- assertStdOutContains(stdOutErrPair,
- "\"GOOG\",\"2018-08-31
10:59:00\",\"9021\",\"1227.1993\",\"1227.215\"");
- }
-
private void testSparkSQLAfterSecondBatch() throws Exception {
Pair<String, String> stdOutErrPair =
executeSparkSQLCommand(SPARKSQL_BATCH2_COMMANDS, true);
assertStdOutContains(stdOutErrPair,
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/ITTestBaseTestcontainers.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/ITTestBaseTestcontainers.java
index 51fdd57b4bfb..256278d1915e 100644
---
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/ITTestBaseTestcontainers.java
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/ITTestBaseTestcontainers.java
@@ -21,6 +21,7 @@ package org.apache.hudi.integ2.testcontainers;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.integ2.testcontainers.service.HiveService;
import org.apache.hudi.integ2.testcontainers.service.SparkService;
+import org.apache.hudi.integ2.testcontainers.service.TrinoService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@@ -28,6 +29,7 @@ import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.ComposeContainer;
import org.testcontainers.containers.ContainerState;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -59,6 +61,7 @@ public abstract class ITTestBaseTestcontainers implements
ContainerProvider {
// Service objects for interacting with different components
protected HiveService hive;
protected SparkService sparkAdhoc1;
+ protected TrinoService trino;
@BeforeAll
public static void setupDockerCompose() {
@@ -75,6 +78,26 @@ public abstract class ITTestBaseTestcontainers implements
ContainerProvider {
Wait.forListeningPort().forPorts(Network.SPARK_MASTER_WEB_UI_PORT)
.withStartupTimeout(Timeouts.CONTAINER_STARTUP))
.withStartupTimeout(Timeouts.CONTAINER_STARTUP);
+
+ // Activate optional compose profiles (e.g. "trino") when requested.
Without this the
+ // profile-gated services stay down, which is the default hive-sync-only
topology.
+ String composeProfiles =
System.getProperty(SystemProps.COMPOSE_PROFILES_PROP, "");
+ if (!composeProfiles.isEmpty()) {
+ environment.withEnv("COMPOSE_PROFILES", composeProfiles);
+ if (composeProfiles.contains(SystemProps.TRINO_PROFILE)) {
+ // Stream the coordinator's log into the test output. When Trino dies
during
+ // startup (plugin load or config errors) the container is torn down
with the
+ // stack, so this stream is the only place the root cause survives.
+ environment.withLogConsumer(Containers.TRINO_COORDINATOR,
+ new
Slf4jLogConsumer(log).withPrefix(Containers.TRINO_COORDINATOR));
+ }
+ }
+ // Point the compose stack at a host-built Trino plugin dir when supplied.
The compose
+ // file falls back to an empty overlay when TRINO_PLUGIN_DIR is unset.
+ String trinoPluginDir =
System.getProperty(SystemProps.TRINO_PLUGIN_DIR_PROP);
+ if (trinoPluginDir != null) {
+ environment.withEnv("TRINO_PLUGIN_DIR", trinoPluginDir);
+ }
environment.start();
log.info("Docker Compose environment started successfully");
@@ -83,7 +106,7 @@ public abstract class ITTestBaseTestcontainers implements
ContainerProvider {
/**
* Tear down the compose stack between test classes. The docker-compose
files publish
- * host ports directly (zookeeper 2181, spark 7077, …), so leaving one
+ * host ports directly (zookeeper 2181, spark 7077, ...), so leaving one
* stack up would make the next class's `@BeforeAll` collide on those host
ports.
* Testcontainers' Ryuk reaper only fires at JVM shutdown, which is too late
when
* failsafe reuses a JVM across classes.
@@ -106,6 +129,32 @@ public abstract class ITTestBaseTestcontainers implements
ContainerProvider {
protected void initializeServices() {
this.hive = new HiveService(this);
this.sparkAdhoc1 = new SparkService(this, Containers.ADHOC_1);
+ // Only wire the Trino service when its profile is active; otherwise the
+ // trinocoordinator container does not exist and getContainer would throw.
+ if (isTrinoProfileActive()) {
+ this.trino = new TrinoService(this);
+ }
+ }
+
+ /**
+ * Returns {@code true} when the {@link SystemProps#COMPOSE_PROFILES_PROP}
system
+ * property activates the {@code trino} compose profile, i.e. the Trino
coordinator
+ * container is part of the running stack.
+ */
+ protected static boolean isTrinoProfileActive() {
+ return System.getProperty(SystemProps.COMPOSE_PROFILES_PROP, "")
+ .contains(SystemProps.TRINO_PROFILE);
+ }
+
+ /**
+ * Skips the test unless the {@code trino} compose profile is active. Use in
the
+ * {@code @BeforeAll} of Trino ITs so they abort cleanly on a hive-sync-only
stack
+ * where the coordinator container is absent.
+ */
+ protected static void assumeTrinoProfile() {
+ Assumptions.assumeTrue(isTrinoProfileActive(),
+ "Test requires the 'trino' compose profile; run with -D"
+ + SystemProps.COMPOSE_PROFILES_PROP + "=" +
SystemProps.TRINO_PROFILE);
}
/**
@@ -120,9 +169,7 @@ public abstract class ITTestBaseTestcontainers implements
ContainerProvider {
/**
* Non-assumption variant of {@link #assumeSpark4Compose()}: returns {@code
true} when the
- * active compose prefix points at a Spark 4.x stack. Use in {@code
@BeforeAll} seeding
- * to conditionally run Spark 4-only fixtures (e.g. VARIANT) without
aborting the whole
- * test class on a Spark 3.5 run.
+ * active compose prefix points at a Spark 4.x stack, without aborting the
caller.
*/
protected static boolean isSpark4Compose() {
String composePrefix = System.getProperty(SystemProps.COMPOSE_PREFIX,
SystemProps.DEFAULT_COMPOSE_PREFIX);
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/TestcontainersConfig.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/TestcontainersConfig.java
index ad403ff6cccd..300fc661968b 100644
---
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/TestcontainersConfig.java
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/TestcontainersConfig.java
@@ -43,6 +43,7 @@ public final class TestcontainersConfig {
public static final String SPARK_MASTER = "sparkmaster";
// Testcontainers appends the replica index, so the adhoc services resolve
as "<name>-1".
public static final String ADHOC_1 = "adhoc-1-1";
+ public static final String TRINO_COORDINATOR = "trinocoordinator";
private Containers() {
}
@@ -66,6 +67,8 @@ public final class TestcontainersConfig {
/** Network endpoints the harness exposes to tests. */
public static final class Network {
public static final int SPARK_MASTER_WEB_UI_PORT = 8080;
+ /** Container-internal Trino HTTP port. Tests exec the CLI inside the
coordinator. */
+ public static final int TRINO_PORT = 8080;
private Network() {
}
@@ -76,6 +79,13 @@ public final class TestcontainersConfig {
public static final Duration CONTAINER_STARTUP = Duration.ofMinutes(5);
public static final int HDFS_MAX_RETRIES = 12;
public static final Duration HDFS_RETRY_INTERVAL = Duration.ofSeconds(10);
+ /**
+ * Trino's slow startup path is plugin discovery + metastore handshake.
The CLI's
+ * first query against an unready coordinator returns a misleading error,
so callers
+ * should retry up to this many times.
+ */
+ public static final int TRINO_READY_MAX_RETRIES = 18;
+ public static final Duration TRINO_READY_RETRY_INTERVAL =
Duration.ofSeconds(10);
private Timeouts() {
}
@@ -88,6 +98,17 @@ public final class TestcontainersConfig {
/** Substring present in compose prefixes that run Spark 4.x (e.g.
"...spark402"). */
public static final String SPARK_4_PREFIX_TOKEN = "spark4";
+ /**
+ * Comma-separated Docker Compose profiles to activate (passed through to
the
+ * compose stack as {@code COMPOSE_PROFILES}). The Trino services live
behind the
+ * {@link #TRINO_PROFILE} profile, so they only start when it is present.
+ */
+ public static final String COMPOSE_PROFILES_PROP = "compose.profiles";
+ /** Host path to the built Trino Hudi plugin, mounted into the coordinator
container. */
+ public static final String TRINO_PLUGIN_DIR_PROP = "trino.plugin.dir";
+ /** Compose profile name that gates the Trino coordinator/worker services.
*/
+ public static final String TRINO_PROFILE = "trino";
+
/**
* Flip to {@code true} (e.g. {@code -Dhudi.integ.hive.verbose=true}) to
route
* Hive logs to the console so exception stack traces show up in test
output.
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/service/TrinoService.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/service/TrinoService.java
new file mode 100644
index 000000000000..4eb3398c7e1f
--- /dev/null
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/service/TrinoService.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.integ2.testcontainers.service;
+
+import org.apache.hudi.integ2.testcontainers.ContainerProvider;
+import org.apache.hudi.integ2.testcontainers.TestcontainersConfig;
+import org.apache.hudi.integ2.testcontainers.command.CommandExecutor;
+import org.apache.hudi.integ2.testcontainers.command.CommandResult;
+
+import lombok.extern.slf4j.Slf4j;
+import org.testcontainers.containers.ContainerState;
+
+/**
+ * Service wrapper for the Trino coordinator. Mirrors {@link HiveService} in
shape, but
+ * execs the bundled {@code trino} CLI inside the coordinator container itself
rather
+ * than from an adhoc Spark container. The coordinator runs a hudi-built image
based on
+ * {@code trinodb/trino:481}, which bundles a modern JDK; the adhoc Spark
images do not,
+ * so {@code execInContainer("trino", ...)} on the coordinator is the reliable
way to
+ * run Trino 481's CLI.
+ *
+ * <p>The default catalog is {@code hudi} (the native trino-hudi connector
registered
+ * by {@code HudiConnectorFactory#getName}) and the default schema is {@code
default}.
+ *
+ * <p>Output format is {@code CSV_UNQUOTED} so substring assertions stay
simple and
+ * match the existing {@link HiveService} ergonomics.
+ */
+@Slf4j
+public class TrinoService {
+
+ private static final String CLI = "trino";
+ private static final String SERVER = "localhost:" +
TestcontainersConfig.Network.TRINO_PORT;
+ private static final String DEFAULT_CATALOG = "hudi";
+ private static final String DEFAULT_SCHEMA = "default";
+
+ private final CommandExecutor executor;
+ private final ContainerState container;
+
+ public TrinoService(ContainerProvider provider) {
+ this.container =
provider.getContainer(TestcontainersConfig.Containers.TRINO_COORDINATOR);
+ this.executor = new CommandExecutor(container);
+ }
+
+ /**
+ * Execute a single Trino SQL statement against the default {@code
hudi.default}
+ * catalog/schema. Returns a {@link CommandResult} so the fluent assertions
used by
+ * other services apply unchanged.
+ */
+ public CommandResult execute(String sql) throws Exception {
+ return execute(DEFAULT_CATALOG, DEFAULT_SCHEMA, sql);
+ }
+
+ /**
+ * Execute a single Trino SQL statement against an explicit catalog/schema.
Useful
+ * for {@code SHOW CATALOGS} or cross-catalog probes where the schema is
irrelevant.
+ */
+ public CommandResult execute(String catalog, String schema, String sql)
throws Exception {
+ String[] cmd = {
+ CLI,
+ "--server", SERVER,
+ "--catalog", catalog,
+ "--schema", schema,
+ "--output-format", "CSV_UNQUOTED",
+ // On failure the CLI prints the full server-side stack to stderr,
which
+ // CommandResult embeds in the assertion message. No effect on success
output.
+ "--debug",
+ "--execute", sql
+ };
+ return executor.executeCommand(cmd);
+ }
+
+ /**
+ * Block until the coordinator is ready to serve queries. Trino reports a
healthy
+ * HTTP {@code /v1/info} well before plugin discovery finishes, so the
cheapest
+ * reliable readiness probe is to actually issue a query.
+ */
+ public void waitUntilReady() throws Exception {
+ int max = TestcontainersConfig.Timeouts.TRINO_READY_MAX_RETRIES;
+ long sleepMs =
TestcontainersConfig.Timeouts.TRINO_READY_RETRY_INTERVAL.toMillis();
+ for (int i = 1; i <= max; i++) {
+ try {
+ execute("system", "runtime", "SELECT 1").expectToSucceed();
+ log.info("Trino coordinator is ready (attempt {}/{})", i, max);
+ return;
+ } catch (Throwable t) {
+ if (!container.isRunning()) {
+ // No point retrying against a dead container; the boot log streamed
by the
+ // ITTestBaseTestcontainers log consumer holds the root cause.
+ throw new RuntimeException(
+ "trinocoordinator container is not running -- Trino likely
crashed at startup;"
+ + " see the trinocoordinator-prefixed log lines above", t);
+ }
+ if (i == max) {
+ throw new RuntimeException(
+ "Trino coordinator did not become ready after " + max + "
retries", t);
+ }
+ log.info("Waiting for Trino coordinator to be ready (attempt {}/{}):
{}", i, max, t.getMessage());
+ Thread.sleep(sleepMs);
+ }
+ }
+ }
+}
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoCustomType.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoCustomType.java
new file mode 100644
index 000000000000..677ee43cd21f
--- /dev/null
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoCustomType.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.integ2.testcontainers.trino;
+
+import org.apache.hudi.integ2.testcontainers.ITTestBaseTestcontainers;
+import org.apache.hudi.integ2.testcontainers.ITTestCustomTypeHiveSync;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import static org.apache.hudi.integ2.testcontainers.TestcontainersConfig.Paths;
+
+/**
+ * Trino read coverage for Hudi's custom logical types (BLOB struct, VECTOR
+ * fixed_len_byte_array, VARIANT), complementing {@link
ITTestCustomTypeHiveSync}
+ * which asserts the same fixtures round-trip through the Hive serde. A flip in
+ * either direction (e.g. VECTOR decoded as array<float> instead of
+ * binary, BLOB struct field projection broken, VARIANT row count off) shows up
+ * here.
+ *
+ * <p>This test reuses the same {@code sparksql-*-sql.commands} fixtures that
+ * {@code ITTestCustomTypeHiveSync} drives. The two run in either order without
+ * cross-contamination because {@code tearDownDockerCompose} drops the whole
+ * stack between classes, so each starts against a fresh HDFS and metastore and
+ * re-seeds its own fixtures.
+ *
+ * <p>The {@code trinocoordinator} service exists only in the spark402 compose
+ * pair, so this class always runs on a Spark 4.x stack and the VARIANT
coverage
+ * is unconditional.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class ITTestTrinoCustomType extends ITTestBaseTestcontainers {
+
+ private static final String BLOB_TEST_PATH =
"/user/hive/warehouse/blob_test";
+ private static final String BLOB_TEST_DF_PATH =
"/user/hive/warehouse/blob_test_df";
+ private static final String VECTOR_TEST_PATH =
"/user/hive/warehouse/vector_test";
+ private static final String VARIANT_TEST_PATH =
"/user/hive/warehouse/variant_test";
+ private static final String SPARKSQL_BLOB_TYPE_SQL_COMMANDS =
+ Paths.DEMO_DIR + "/sparksql-blob-type-sql.commands";
+ private static final String SPARKSQL_BLOB_TYPE_DF_COMMANDS =
+ Paths.DEMO_DIR + "/sparksql-blob-type-df.commands";
+ private static final String SPARKSQL_VECTOR_TYPE_SQL_COMMANDS =
+ Paths.DEMO_DIR + "/sparksql-vector-type-sql.commands";
+ private static final String SPARKSQL_VARIANT_TYPE_SQL_COMMANDS =
+ Paths.DEMO_DIR + "/sparksql-variant-type-sql.commands";
+
+ @BeforeAll
+ public void setupOnce() throws Exception {
+ assumeTrinoProfile();
+ initializeServices();
+ waitForHdfs();
+ sparkAdhoc1.executeShellCommand("/bin/bash " +
Paths.DEMO_SETUP).expectToSucceed();
+ sparkAdhoc1.executeSQLFile(SPARKSQL_BLOB_TYPE_SQL_COMMANDS)
+ .expectToSucceed()
+ .assertStdOutContainsLine("BLOB_SQL_TEST_SUCCESS");
+ // The DF fixture writes blob_test_df with the INLINE branch of the BLOB
struct
+ // (data field non-null, reference null). The SQL fixture exercises only
the
+ // OUT_OF_LINE branch, so seeding both gives Trino read coverage of both
shapes.
+ sparkAdhoc1.executeSQLFile(SPARKSQL_BLOB_TYPE_DF_COMMANDS)
+ .expectToSucceed()
+ .assertStdOutContainsLine("BLOB_DF_TEST_SUCCESS");
+ sparkAdhoc1.executeSQLFile(SPARKSQL_VECTOR_TYPE_SQL_COMMANDS)
+ .expectToSucceed()
+ .assertStdOutContainsLine("VECTOR_SQL_TEST_SUCCESS");
+ sparkAdhoc1.executeSQLFile(SPARKSQL_VARIANT_TYPE_SQL_COMMANDS)
+ .expectToSucceed()
+ .assertStdOutContainsLine("VARIANT_SQL_TEST_SUCCESS");
+ trino.waitUntilReady();
+ }
+
+ @AfterAll
+ public void clean() throws Exception {
+ // JUnit runs @AfterAll even when the @BeforeAll assumption aborted
setupOnce()
+ // before initializeServices(); nothing was seeded then, so nothing to
clean.
+ if (sparkAdhoc1 == null) {
+ return;
+ }
+ // -f silently skips non-existent paths so cleanup is safe even when
seeding
+ // failed partway and some of these tables were never created.
+ sparkAdhoc1.executeShellCommand("hdfs dfs -rm -R -f "
+ + BLOB_TEST_PATH + " " + BLOB_TEST_DF_PATH + " "
+ + VECTOR_TEST_PATH + " " + VARIANT_TEST_PATH).expectToSucceed();
+ }
+
+ // ---------- BLOB OUT_OF_LINE (blob_test) ----------
+
+ @Test
+ public void testTrinoCountBlob() throws Exception {
+ // Post-DELETE state of sparksql-blob-type-sql.commands is 2 rows (id=1
updated,
+ // id=2 merged, id=3 inserted then deleted) - parity with the Hive count
assertion
+ // in ITTestCustomTypeHiveSync#testBlobTypeWithHiveSyncSQL.
+ trino.execute("SELECT count(*) FROM blob_test")
+ .expectToSucceed()
+ .assertStdOutContains("2");
+ }
+
+ @Test
+ public void testTrinoProjectsBlobUpdatedRow() throws Exception {
+ // Full per-row shape for id=1 (post-UPDATE state): type discriminator +
+ // every reference subfield + the OUT_OF_LINE invariant that data IS NULL.
+ // One query, one substring assertion - catches column-order shifts,
+ // nested-struct field renames, and per-field decoding bugs.
+ trino.execute("SELECT blob_data.type, blob_data.data IS NULL, "
+ + "blob_data.reference.external_path, blob_data.reference.offset, "
+ + "blob_data.reference.length, blob_data.reference.managed "
+ + "FROM blob_test WHERE id = 1")
+ .expectToSucceed()
+ .assertStdOutContains("OUT_OF_LINE,true,blobs/updated-1,10,100,true");
+ }
+
+ @Test
+ public void testTrinoProjectsBlobMergedRow() throws Exception {
+ // id=2 was MATCHED by the MERGE clause and rewritten to 'blobs/merged-2'.
+ // Same full-shape assertion as id=1 - confirms both UPDATE and MERGE write
+ // paths land at an identical on-disk OUT_OF_LINE shape.
+ trino.execute("SELECT blob_data.type, blob_data.data IS NULL, "
+ + "blob_data.reference.external_path, blob_data.reference.offset, "
+ + "blob_data.reference.length, blob_data.reference.managed "
+ + "FROM blob_test WHERE id = 2")
+ .expectToSucceed()
+ .assertStdOutContains("OUT_OF_LINE,true,blobs/merged-2,20,200,true");
+ }
+
+ @Test
+ public void testTrinoBlobDeletedRowAbsent() throws Exception {
+ // id=3 was MERGE-inserted into dt=2024-01-02 then DELETEd. A DELETE that
+ // leaves the row visible (e.g. tombstone not honored on read) shows up as
+ // count = 1 here. Pairs with testTrinoCountBlob = 2 (total post-delete)
+ // to catch the case where DELETE silently no-ops.
+ trino.execute("SELECT count(*) FROM blob_test WHERE id = 3")
+ .expectToSucceed()
+ .assertStdOutContains("0");
+ }
+
+ @Test
+ public void testTrinoBlobEmptiedPartitionInvisible() throws Exception {
+ // MERGE created dt=2024-01-02 (id=3), then DELETE emptied it. Grouping by
the
+ // partition column must return exactly the data-bearing partition with its
+ // full row count. Not a partition-pruning assertion: GROUP BY only emits
+ // groups for partitions that still hold rows, and dt=2024-01-02 stays
+ // registered in the metastore either way (see ITTestCustomTypeHiveSync).
+ // What it does catch is a mis-decoded partition column, or a deleted row
+ // resurfacing under its old partition.
+ trino.execute("SELECT dt, count(*) FROM blob_test GROUP BY dt ORDER BY dt")
+ .expectToSucceed()
+ .assertStdOutContains("2024-01-01,2")
+ .assertStdOutContains("2024-01-02", 0);
+ }
+
+ // ---------- BLOB INLINE (blob_test_df) ----------
+
+ @Test
+ public void testTrinoCountBlobInline() throws Exception {
+ // Post-DELETE state of sparksql-blob-type-df.commands is 2 rows (id=1
kept,
+ // id=2 updated to "updated payload", id=3 upserted then deleted). Parity
+ // with testTrinoCountBlob for the INLINE-shape table.
+ trino.execute("SELECT count(*) FROM blob_test_df")
+ .expectToSucceed()
+ .assertStdOutContains("2");
+ }
+
+ @Test
+ public void testTrinoProjectsBlobInlineRow() throws Exception {
+ // INLINE shape for id=1 (never mutated): type=INLINE, data is the UTF-8
+ // seed "hello world", and the INLINE invariant that reference IS NULL.
+ // from_utf8(data) is the right decoder - raw cast(varbinary as varchar)
+ // is rejected by Trino.
+ trino.execute("SELECT blob_data.type, from_utf8(blob_data.data), "
+ + "blob_data.reference IS NULL FROM blob_test_df WHERE id = 1")
+ .expectToSucceed()
+ .assertStdOutContains("INLINE,hello world,true");
+ }
+
+ @Test
+ public void testTrinoProjectsBlobInlineUpdatedRow() throws Exception {
+ // id=2 was UPSERT-rewritten to "updated payload" in the DF fixture. Same
+ // full-shape assertion as id=1 - confirms the UPSERT write path for the
+ // INLINE branch round-trips end-to-end through Trino.
+ trino.execute("SELECT blob_data.type, from_utf8(blob_data.data), "
+ + "blob_data.reference IS NULL FROM blob_test_df WHERE id = 2")
+ .expectToSucceed()
+ .assertStdOutContains("INLINE,updated payload,true");
+ }
+
+ @Test
+ public void testTrinoBlobInlineDeletedRowAbsent() throws Exception {
+ // id=3 was upserted into dt=2024-01-02 then DELETEd in the DF fixture.
+ // Parity with testTrinoBlobDeletedRowAbsent for the INLINE-shape table.
+ trino.execute("SELECT count(*) FROM blob_test_df WHERE id = 3")
+ .expectToSucceed()
+ .assertStdOutContains("0");
+ }
+
+ // ---------- VECTOR (vector_test) ----------
+
+ @Test
+ public void testTrinoCountVector() throws Exception {
+ // Post-DELETE state of sparksql-vector-type-sql.commands is 2 rows.
+ // Parity with testTrinoCountBlob / testTrinoCountVariant.
+ trino.execute("SELECT count(*) FROM vector_test")
+ .expectToSucceed()
+ .assertStdOutContains("2");
+ }
+
+ @Test
+ public void testTrinoVectorDeletedRowAbsent() throws Exception {
+ // id=3 was MERGE-inserted then DELETEd in the vector fixture. Parity with
+ // the BLOB/VARIANT delete-absence checks.
+ trino.execute("SELECT count(*) FROM vector_test WHERE id = 3")
+ .expectToSucceed()
+ .assertStdOutContains("0");
+ }
+
+ @Test
+ public void testTrinoVectorRoundTripsAsBinary() throws Exception {
+ // Per RFC-99, VECTOR(3) is stored on disk as fixed_len_byte_array(12) and
Hive
+ // sync maps it to BINARY. The native plugin should expose the column as
VARBINARY
+ // of the same 12 bytes (3 floats * 4 bytes). length() returning 12
confirms the
+ // round-trip; a return of 3 would mean the plugin decoded it as
array<float>,
+ // a real regression worth a separate ticket. Pairs with the Hive
assertion at
+ // ITTestCustomTypeHiveSync:228-235.
+ trino.execute("SELECT length(embedding) FROM vector_test WHERE id = 1")
+ .expectToSucceed()
+ .assertStdOutContains("12");
+ }
+
+ @Test
+ public void testTrinoVectorBytesDecodeToExpectedFloats() throws Exception {
+ // The 12 bytes of VECTOR(3) are 3 IEEE-754 floats in little-endian
(Parquet's
+ // default). reverse(substr(..., n, 4)) flips each 4-byte chunk to
big-endian
+ // so from_ieee754_32 returns the actual value. round(., 1) sidesteps
float-
+ // precision noise in Trino's CSV rendering (0.9f decodes to ~0.90000004).
+ // For id=1's post-UPDATE state the fixture writes (0.9f, 0.8f, 0.7f).
+ trino.execute("SELECT round(from_ieee754_32(reverse(substr(embedding, 1,
4))), 1), "
+ + "round(from_ieee754_32(reverse(substr(embedding, 5, 4))), 1), "
+ + "round(from_ieee754_32(reverse(substr(embedding, 9, 4))), 1) "
+ + "FROM vector_test WHERE id = 1")
+ .expectToSucceed()
+ .assertStdOutContains("0.9,0.8,0.7");
+ }
+
+ @Test
+ public void testTrinoVectorMergedRowDecodes() throws Exception {
+ // id=2 was MATCHED by the MERGE clause and rewritten to (0.41f, 0.51f,
0.61f).
+ // round(., 2) keeps it readable; complements id=1's UPDATE path to confirm
+ // both write paths land at the same on-disk layout.
+ trino.execute("SELECT round(from_ieee754_32(reverse(substr(embedding, 1,
4))), 2), "
+ + "round(from_ieee754_32(reverse(substr(embedding, 5, 4))), 2), "
+ + "round(from_ieee754_32(reverse(substr(embedding, 9, 4))), 2) "
+ + "FROM vector_test WHERE id = 2")
+ .expectToSucceed()
+ .assertStdOutContains("0.41,0.51,0.61");
+ }
+
+ @Test
+ public void testTrinoVectorAllRowsAreFixedLength12() throws Exception {
+ // Invariant: every row's embedding is exactly 12 bytes. count(DISTINCT
length(...))
+ // = 1 AND min/max = 12 catches the case where some rows decode at a
different
+ // width (e.g. an older row written before a layout fix). Stronger than the
+ // single-row length() check in testTrinoVectorRoundTripsAsBinary.
+ trino.execute("SELECT count(DISTINCT length(embedding)),
min(length(embedding)), "
+ + "max(length(embedding)) FROM vector_test")
+ .expectToSucceed()
+ .assertStdOutContains("1,12,12");
+ }
+
+ // ---------- VARIANT (variant_test) ----------
+
+ @Test
+ public void testTrinoCountVariant() throws Exception {
+ // Post-DELETE state of sparksql-variant-type-sql.commands is 2 rows (id=1
+ // updated, id=2 merged, id=3 inserted then deleted) - parity with the Hive
+ // count assertion in
ITTestCustomTypeHiveSync#testVariantTypeWithHiveSyncSQL.
+ // count(*) doesn't deserialize the variant column so it's safe even if the
+ // plugin's variant decoding has gaps.
+ trino.execute("SELECT count(*) FROM variant_test")
+ .expectToSucceed()
+ .assertStdOutContains("2");
+ }
+
+ @Test
+ public void testTrinoIntrospectsVariantColumn() throws Exception {
+ // Schema-level smoke: the variant_data column must surface in Trino's
view of
+ // the table. Catches metastore-side regressions where Hive sync drops or
+ // mistypes the variant column entirely, separately from the
+ // value-projection question (which depends on how the plugin maps
VARIANT).
+ trino.execute("DESCRIBE variant_test")
+ .expectToSucceed()
+ .assertStdOutContains("variant_data");
+ }
+
+ @Test
+ public void testTrinoProjectsVariantValue() throws Exception {
+ // The native trino-hudi plugin exposes VARIANT as ROW(metadata VARBINARY,
+ // value VARBINARY) with no top-level JSON/string decoding. But Spark's
+ // Variant binary format stores leaf string values as UTF-8 bytes inside
+ // the value component, so we can decode the bytes with from_utf8() and
+ // LIKE-match the seeded payload. Non-UTF8 framing bytes around the leaf
+ // become U+FFFD replacement chars, which the wildcard tolerates. This is
+ // real content round-trip: write {"key":"value1-updated"}, read the same
+ // payload back through the plugin.
+ trino.execute("SELECT from_utf8(variant_data.value) LIKE
'%value1-updated%' "
+ + "FROM variant_test WHERE id = 1")
+ .expectToSucceed()
+ .assertStdOutContains("true");
+ }
+
+ @Test
+ public void testTrinoProjectsVariantMergedRow() throws Exception {
+ // Same content-level round-trip for the MERGE-rewritten row. id=2's seed
+ // is {"key":"value2-merged"}; verifying that exact payload survives the
+ // MERGE write path -> hive sync -> Trino read end-to-end.
+ trino.execute("SELECT from_utf8(variant_data.value) LIKE '%value2-merged%'
"
+ + "FROM variant_test WHERE id = 2")
+ .expectToSucceed()
+ .assertStdOutContains("true");
+ }
+
+ @Test
+ public void testTrinoVariantValuesDifferAcrossRows() throws Exception {
+ // Invariant: id=1 ({"key":"value1-updated"}) and id=2
({"key":"value2-merged"})
+ // must produce different value bytes. Trino's count(DISTINCT ...) supports
+ // VARBINARY natively, so no base64 wrapping is needed. Catches the
+ // "projection returns same bytes for every row" regression that per-row
+ // content matches would silently miss.
+ trino.execute("SELECT count(DISTINCT variant_data.value) FROM
variant_test")
+ .expectToSucceed()
+ .assertStdOutContains("2");
+ }
+
+ @Test
+ public void testTrinoVariantDeletedRowAbsent() throws Exception {
+ // id=3 was MERGE-inserted into dt=2024-01-02 then DELETEd. Mirrors
+ // testTrinoBlobDeletedRowAbsent for the variant table - catches the case
+ // where DELETE silently no-ops on a Variant-bearing row.
+ trino.execute("SELECT count(*) FROM variant_test WHERE id = 3")
+ .expectToSucceed()
+ .assertStdOutContains("0");
+ }
+
+ @Test
+ public void testTrinoVariantEmptiedPartitionInvisible() throws Exception {
+ // Same shape as the BLOB table: dt=2024-01-02 held only id=3, so after the
+ // DELETE the grouped read must return the data-bearing partition alone. As
+ // above this asserts per-partition row visibility, not partition pruning.
+ trino.execute("SELECT dt, count(*) FROM variant_test GROUP BY dt ORDER BY
dt")
+ .expectToSucceed()
+ .assertStdOutContains("2024-01-01,2")
+ .assertStdOutContains("2024-01-02", 0);
+ }
+}
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoSmoke.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoSmoke.java
new file mode 100644
index 000000000000..2c5b44268189
--- /dev/null
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoSmoke.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.integ2.testcontainers.trino;
+
+import org.apache.hudi.integ2.testcontainers.ITTestBaseTestcontainers;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Smoke coverage for the native trino-hudi connector running inside the integ2
+ * testcontainers harness. Cheapest signal that the plugin loaded, the
metastore
+ * is reachable, and the CLI can round-trip a query.
+ *
+ * <p>Skipped unless the {@code trino} compose profile is active (see
+ * {@link #assumeTrinoProfile()}), since the coordinator container only starts
then.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class ITTestTrinoSmoke extends ITTestBaseTestcontainers {
+
+ @BeforeAll
+ public void setupOnce() throws Exception {
+ assumeTrinoProfile();
+ initializeServices();
+ trino.waitUntilReady();
+ }
+
+ @Test
+ public void testShowCatalogsListsHudi() throws Exception {
+ // Asserts the native plugin registered. With connector.name=hudi (per
+ // HudiConnectorFactory#getName) the catalog appears under that name; if
the
+ // plugin failed to load the catalog file would have made Trino fail to
start
+ // and we will never reach this assertion.
+ trino.execute("system", "runtime", "SHOW CATALOGS")
+ .expectToSucceed()
+ .assertStdOutContains("hudi");
+ }
+
+ @Test
+ public void testShowSchemasFromHudiReachesMetastore() throws Exception {
+ // The `default` schema is created by Hive at first contact with the
metastore.
+ // Asserting it appears here proves the connector can talk to
thrift://hivemetastore:9083.
+ trino.execute("hudi", "default", "SHOW SCHEMAS")
+ .expectToSucceed()
+ .assertStdOutContains("default");
+ }
+
+ @Test
+ public void testSelectOneRoundtrip() throws Exception {
+ trino.execute("system", "runtime", "SELECT 1")
+ .expectToSucceed()
+ .assertStdOutContains("1");
+ }
+}
diff --git
a/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoStockTicks.java
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoStockTicks.java
new file mode 100644
index 000000000000..43dfdd18134e
--- /dev/null
+++
b/hudi-integ-test/src/test/java/org/apache/hudi/integ2/testcontainers/trino/ITTestTrinoStockTicks.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.integ2.testcontainers.trino;
+
+import org.apache.hudi.integ2.testcontainers.ITTestBaseTestcontainers;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import static org.apache.hudi.integ2.testcontainers.TestcontainersConfig.Paths;
+
+/**
+ * End-to-end coverage that the native trino-hudi connector can read both COW
and
+ * MOR tables that came from Spark + Hive sync. Mirrors the retired
+ * {@code docker/demo/trino-batch1.commands} demo flow (removed together with
the
+ * rest of the legacy trino-coordinator path) but uses a self-contained
spark-sql
+ * fixture (see {@code sparksql-stock-ticks-trino.commands}) instead of the
full
+ * Kafka/streaming pipeline, which integ2 doesn't otherwise exercise.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class ITTestTrinoStockTicks extends ITTestBaseTestcontainers {
+
+ private static final String STOCK_TICKS_COW_PATH =
"/user/hive/warehouse/stock_ticks_cow";
+ private static final String STOCK_TICKS_MOR_PATH =
"/user/hive/warehouse/stock_ticks_mor";
+ private static final String SPARKSQL_STOCK_TICKS_COMMANDS =
+ Paths.DEMO_DIR + "/sparksql-stock-ticks-trino.commands";
+
+ @BeforeAll
+ public void setupOnce() throws Exception {
+ assumeTrinoProfile();
+ initializeServices();
+ waitForHdfs();
+ sparkAdhoc1.executeShellCommand("/bin/bash " +
Paths.DEMO_SETUP).expectToSucceed();
+ sparkAdhoc1.executeSQLFile(SPARKSQL_STOCK_TICKS_COMMANDS)
+ .expectToSucceed()
+ .assertStdOutContainsLine("STOCK_TICKS_COW_SETUP_SUCCESS")
+ .assertStdOutContainsLine("STOCK_TICKS_MOR_SETUP_SUCCESS")
+ .assertStdOutContainsLine("STOCK_TICKS_TRINO_SETUP_SUCCESS");
+ trino.waitUntilReady();
+ }
+
+ @AfterAll
+ public void clean() throws Exception {
+ // JUnit runs @AfterAll even when the @BeforeAll assumption aborted
setupOnce()
+ // before initializeServices(); nothing was seeded then, so nothing to
clean.
+ if (sparkAdhoc1 == null) {
+ return;
+ }
+ sparkAdhoc1.executeShellCommand("hdfs dfs -rm -R -f "
+ + STOCK_TICKS_COW_PATH + " " + STOCK_TICKS_MOR_PATH).expectToSucceed();
+ }
+
+ // ---------- Queries reproduced from the retired
docker/demo/trino-batch1.commands ----------
+
+ @Test
+ public void testTrinoReadsCowMaxTs() throws Exception {
+ // Original: select symbol, max(ts) from stock_ticks_cow group by symbol
HAVING symbol = 'GOOG'
+ trino.execute("SELECT symbol, max(ts) FROM stock_ticks_cow GROUP BY symbol
HAVING symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG")
+ .assertStdOutContains("2018-08-31 10:29:00");
+ }
+
+ @Test
+ public void testTrinoReadsMorRoMaxTs() throws Exception {
+ // Hive sync produces stock_ticks_mor_ro (RO view of base files). The
fixture's
+ // UPDATE (ts 10:59:00) lives only in a log file, so _ro must keep serving
the
+ // 10:29:00 base row - a 10:59:00 here means log records leaked into the
RO view.
+ trino.execute("SELECT symbol, max(ts) FROM stock_ticks_mor_ro GROUP BY
symbol HAVING symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG")
+ .assertStdOutContains("2018-08-31 10:29:00")
+ .assertStdOutContains("2018-08-31 10:59:00", 0);
+ }
+
+ @Test
+ public void testTrinoReadsCowProjectedColumns() throws Exception {
+ // open == close == 1230.50 in the seed row, so "1230.5" appears twice in
CSV output.
+ trino.execute("SELECT symbol, ts, volume, open, close FROM stock_ticks_cow
WHERE symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG,2018-08-31 10:29:00,6330,1230.5,1230.5");
+ }
+
+ @Test
+ public void testTrinoReadsMorRoProjectedColumns() throws Exception {
+ // Same symbol-count pin as the _rt case below: the base row assert on its
own
+ // would still pass with the log row returned next to it.
+ trino.execute("SELECT symbol, ts, volume, open, close FROM
stock_ticks_mor_ro WHERE symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG,2018-08-31 10:29:00,6330,1230.5,1230.5")
+ .assertStdOutContains("GOOG", 1);
+ }
+
+ @Test
+ public void testTrinoReadsMorRtMergedMaxTs() throws Exception {
+ // The fixture's UPDATE lands as a log-only delta; the _rt view must merge
it
+ // on read. Pairs with testTrinoReadsMorRoMaxTs pinning _ro to the base
row,
+ // so together they prove the connector takes different read paths for the
+ // two views instead of serving base files for both.
+ trino.execute("SELECT symbol, max(ts) FROM stock_ticks_mor_rt GROUP BY
symbol HAVING symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG")
+ .assertStdOutContains("2018-08-31 10:59:00");
+ }
+
+ @Test
+ public void testTrinoReadsMorRtMergedProjectedColumns() throws Exception {
+ // Full merged row: every non-key column must come from the log record.
The row
+ // assert alone would still pass if the base row came back alongside it,
so pin
+ // the symbol count too - exactly one GOOG row may survive the merge.
+ trino.execute("SELECT symbol, ts, volume, open, close FROM
stock_ticks_mor_rt WHERE symbol = 'GOOG'")
+ .expectToSucceed()
+ .assertStdOutContains("GOOG,2018-08-31 10:59:00,9021,1227.25,1227.5")
+ .assertStdOutContains("GOOG", 1);
+ }
+}
diff --git a/hudi-trino/README.md b/hudi-trino/README.md
index c163121284dc..72c848ba2c18 100644
--- a/hudi-trino/README.md
+++ b/hudi-trino/README.md
@@ -45,6 +45,48 @@ mvn -Phudi-trino,hudi-trino-tests -pl hudi-trino test
CI follows the same two steps: `.github/workflows/hudi_trino_ci.yml` installs
the test-jars from a source checkout of the pinned Trino tag, then runs with
both profiles enabled.
+## End-to-end tests (docker)
+
+The testcontainers E2E suite (`hudi-integ-test`, classes `ITTestTrino*` under
+`org.apache.hudi.integ2.testcontainers.trino`) runs Trino queries against a
real
+HDFS + Hive metastore + Spark stack. The Trino container image bakes in a
plugin
+directory assembled by the in-repo shim at `docker/trino/shim/` -- a
standalone Maven
+project mirroring the upstream `trinodb/trino` `plugin/trino-hudi` shim
planned by
+RFC-105 (not yet released upstream). CI runs the same flow via
+`.github/workflows/hudi_trino_e2e.yml`.
+
+Local flow:
+
+```
+# 1. JDK 17: full reactor incl. the integ-test bundles the containers mount
+mvn clean install -T 2 -Dscala-2.13 -Dscala.binary.version=2.13 -Dspark4.0
-Dflink1.20 \
+ -Pintegration-tests -DskipTests=true -Ddocker.compose.skip=true
+
+# 2. JDK 25: the connector
+mvn -Phudi-trino -pl hudi-trino install -Dmaven.test.skip=true
+
+# 3. JDK 25: assemble the plugin dir (package, NOT install -- installing would
+# shadow the real io.trino:trino-hudi release coordinates in the local m2).
+# dep.hudi.version comes from the reactor pom: the shim sits outside the
+# reactor, so cut_release_branch.sh cannot bump its literal default.
+HUDI_VERSION=$(mvn -q -ntp help:evaluate -Dexpression=project.version
-DforceStdout)
+mvn -f docker/trino/shim/pom.xml clean package -DskipTests
-Ddep.hudi.version="$HUDI_VERSION"
+
+# 4. Build the Trino image (locally tagged; never published)
+docker/trino/build_image.sh --plugin-dir
docker/trino/shim/target/trino-hudi-481
+
+# 5. JDK 17: run the suite (only the spark402 compose pair has the trino
service)
+mvn verify -pl hudi-integ-test -Dscala-2.13 -Dscala.binary.version=2.13
-Dspark4.0 \
+ -Pintegration-tests -DskipITs=false -Ddocker.compose.skip=true \
+ -Dit.test='ITTestTrino*' -Dcompose.profiles=trino \
+ -Dspark.docker.compose.prefix=docker-compose_hadoop340_hive2310_spark402
+```
+
+Fast iteration loop: after changing connector code, redo steps 2-3, then add
+`-Dtrino.plugin.dir=$PWD/docker/trino/shim/target/trino-hudi-481` to step 5.
The
+container's overlay entrypoint swaps the freshly built plugin dir in at start,
so the
+image rebuild (step 4) is skipped.
+
## IDE setup
Only this module needs JDK 25. Leave the rest of Hudi on its native JDK (11 or
17) so you are not toggling the project default.