diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md
index 793a2a1f375c..df79990a332b 100644
--- a/.github/DEVELOPMENT.md
+++ b/.github/DEVELOPMENT.md
@@ -1,28 +1,34 @@
# Development
+In this document you can find information about developing Trino.
+
+* [Trino organization](#trino-organization)
+* [Trino developer guide](#trino-developer-guide)
+* [Code style](#code-style)
+* [Additional IDE configuration](#additional-ide-configuration)
+* [Building docs](#building-docs)
+* [Building the Web UI](#building-the-web-ui)
+* [Releases](#releases)
+
+## Trino organization
+
Learn about development for all Trino organization projects:
* [Vision](https://trino.io/development/vision)
* [Contribution process](https://trino.io/development/process#contribution-process)
-* [Pull request and commit guidelines](https://trino.io/development/process#pull-request-and-commit-guidelines-)
-* [Release note guidelines](https://trino.io/development/process#release-note-guidelines-)
+* [Pull request and commit guidelines](https://trino.io/development/process#pull-request-and-commit-guidelines)
+* [Release note guidelines](https://trino.io/development/process#release-note-guidelines)
Further information in the [development section of the
website](https://trino.io/development) includes different roles, like
contributors, reviewers, and maintainers, related processes, and other aspects.
+## Trino developer guide
+
See [the Trino developer guide](https://trino.io/docs/current/develop.html) for
-information about the SPI, implementing connectors and other plugins plugins,
+information about the SPI, implementing connectors and other plugins,
the client protocol, writing tests and other lower level details.
-More information about writing and building the documentation can be found in
-the [docs module](../docs).
-
-* [Code style](#code-style)
-* [Additional IDE configuration](#additional-ide-configuration)
-* [Building the Web UI](#building-the-web-ui)
-* [CI pipeline](#ci-pipeline)
-
## Code Style
We recommend you use IntelliJ as your IDE. The code style template for the
@@ -214,6 +220,11 @@ with `@Language`:
- Local variables which otherwise would not be properly recognized by IDE for
language injection.
+## Building docs
+
+Information about writing and building the documentation can be found in
+the [docs module](../docs).
+
## Building the Web UI
The Trino Web UI is composed of several React components and is written in JSX
diff --git a/.github/actions/compile-commit/action.yml b/.github/actions/compile-commit/action.yml
index 3cfda30fcc51..d01b0c37fb35 100644
--- a/.github/actions/compile-commit/action.yml
+++ b/.github/actions/compile-commit/action.yml
@@ -22,7 +22,7 @@ runs:
key: compile-commit-success-${{ github.event.pull_request.head.repo.full_name }}-${{ steps.repo-hash.outputs.tree-hash }}
- uses: ./.github/actions/setup
with:
- cleanup-node: false
+ cleanup-node: true
if: steps.check-compile-commit-success.outputs.cache-hit != 'true'
- name: Check if a specified commit compiles
shell: bash
diff --git a/.github/bin/build-matrix-from-impacted.py b/.github/bin/build-matrix-from-impacted.py
index 315d73281a4f..56debbae742a 100755
--- a/.github/bin/build-matrix-from-impacted.py
+++ b/.github/bin/build-matrix-from-impacted.py
@@ -98,6 +98,9 @@ def build(matrix_file, impacted_file, output_file):
def check_modules(modules, impacted):
if isinstance(modules, str):
modules = [modules]
+ # `impacted` can be empty when GIB detected no changes, but also when GIB was not run at all.
+ # The latter is the case on builds on master branch. For these builds we want to run all tests,
+ # so we don't filter out any modules.
if impacted and not any(module in impacted for module in modules):
return None
# concatenate because matrix values should be primitives
diff --git a/.github/config/labeler-config.yml b/.github/config/labeler-config.yml
index 0d380fe8f99c..4f44778847d5 100644
--- a/.github/config/labeler-config.yml
+++ b/.github/config/labeler-config.yml
@@ -67,6 +67,10 @@ kafka:
- changed-files:
- any-glob-to-any-file: 'plugin/trino-kafka/**'
+lakehouse:
+ - changed-files:
+ - any-glob-to-any-file: 'plugin/trino-lakehouse/**'
+
loki:
- changed-files:
- any-glob-to-any-file: 'plugin/trino-loki/**'
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 1b458bd70115..abe810f59048 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,13 +4,10 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- groups:
- dependency-updates:
- applies-to: version-updates
- update-types:
- - major
- - minor
- - patch
- security-updates:
- applies-to: security-updates
- dependency-type: production
+ - package-ecosystem: "maven"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ assignees:
+ - wendigo
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c14b305db85..ed492b5913d7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,9 +5,6 @@ on:
branches:
- master
pull_request:
- paths-ignore:
- - 'docs/**'
- - '**.md'
repository_dispatch:
types: [test-with-secrets-command]
@@ -50,6 +47,22 @@ concurrency:
cancel-in-progress: true
jobs:
+ path-filters:
+ runs-on: ubuntu-latest
+ outputs:
+ docs: ${{ steps.filter.outputs.docs }}
+ non_docs: ${{ steps.filter.outputs.non_docs }}
+ steps:
+ - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
+ id: filter
+ if: github.event_name == 'pull_request'
+ with:
+ # Note: `docs` output is currently unused. The docs changes are tested by maven-checks job, leveraging GIB impact analysis.
+ # TODO remove use of non_docs filters and remove `path-filters` job. Use GIB impact analysis to guard job skipping.
+ filters: |
+ docs: 'docs/**'
+ non_docs: '!docs/**'
+
maven-checks:
runs-on: ubuntu-latest
name: maven-checks ${{ matrix.java-version }}
@@ -69,7 +82,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: ${{ matrix.cache }}
java-version: ${{ matrix.java-version }}
@@ -89,6 +102,8 @@ jobs:
run: rm -rf ~/.m2/repository/io/trino/trino-*
artifact-checks:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
@@ -100,7 +115,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: 'restore'
cleanup-node: true
@@ -122,8 +137,9 @@ jobs:
run: core/docker/build.sh
check-commits-dispatcher:
+ needs: path-filters
+ if: github.event_name == 'pull_request' && needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
- if: github.event_name == 'pull_request'
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
@@ -154,7 +170,7 @@ jobs:
check-commit:
needs: check-commits-dispatcher
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 20
if: github.event_name == 'pull_request' && needs.check-commits-dispatcher.outputs.matrix != ''
strategy:
fail-fast: false
@@ -175,6 +191,8 @@ jobs:
base_ref: ${{ github.event.pull_request.base.ref }}
error-prone-checks:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
@@ -186,7 +204,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
java-version: 25
@@ -203,6 +221,8 @@ jobs:
-pl '!:trino-docs,!:trino-server'
test-jdbc-compatibility:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -214,9 +234,10 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
+ cleanup-node: 'true'
- name: Maven Install
run: |
export MAVEN_OPTS="${MAVEN_INSTALL_OPTS}"
@@ -242,6 +263,8 @@ jobs:
upload-heap-dump: ${{ env.SECRETS_PRESENT == '' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
hive-tests:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -259,7 +282,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
- name: Install Hive Module
@@ -298,6 +321,8 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
test-other-modules:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
@@ -309,7 +334,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
cleanup-node: true
@@ -385,6 +410,8 @@ jobs:
upload-heap-dump: ${{ env.SECRETS_PRESENT == '' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
build-test-matrix:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -397,7 +424,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
- name: Update PR check
@@ -412,7 +439,7 @@ jobs:
- name: Maven validate
run: |
export MAVEN_OPTS="${MAVEN_INSTALL_OPTS}"
- $MAVEN validate ${MAVEN_FAST_INSTALL} ${MAVEN_GIB} -Dgib.logImpactedTo=gib-impacted.log -P disable-check-spi-dependencies -pl '!:trino-docs'
+ $MAVEN validate ${MAVEN_FAST_INSTALL} ${MAVEN_GIB} -Dgib.logImpactedTo=gib-impacted.log -P disable-check-spi-dependencies
- name: Set matrix
id: set-matrix
run: |
@@ -489,6 +516,7 @@ jobs:
- { modules: plugin/trino-snowflake }
- { modules: plugin/trino-snowflake, profile: cloud-tests }
- { modules: plugin/trino-sqlserver }
+ - { modules: plugin/trino-teradata }
- { modules: plugin/trino-vertica }
- { modules: testing/trino-faulttolerant-tests, profile: default }
- { modules: testing/trino-faulttolerant-tests, profile: test-fault-tolerant-delta }
@@ -517,10 +545,10 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
- cleanup-node: ${{ format('{0}', matrix.modules == 'plugin/trino-singlestore' || matrix.modules == 'plugin/trino-exasol') }}
+ cleanup-node: ${{ format('{0}', matrix.modules == 'plugin/trino-singlestore' || matrix.modules == 'plugin/trino-exasol' || matrix.modules == 'plugin/trino-oracle') }}
java-version: ${{ matrix.jdk != '' && matrix.jdk || '25' }}
- name: Maven Install
run: |
@@ -539,6 +567,7 @@ jobs:
&& ! (contains(matrix.modules, 'trino-filesystem-gcs') && contains(matrix.profile, 'cloud-tests'))
&& ! (contains(matrix.modules, 'trino-filesystem-s3') && contains(matrix.profile, 'cloud-tests'))
&& ! (contains(matrix.modules, 'trino-hdfs') && contains(matrix.profile, 'cloud-tests'))
+ && ! (contains(matrix.modules, 'trino-teradata'))
run: $MAVEN test ${MAVEN_TEST} -pl ${{ matrix.modules }} ${{ matrix.profile != '' && format('-P {0}', matrix.profile) || '' }}
# Additional tests for selected modules
- name: HDFS file system cache isolated JVM tests
@@ -764,6 +793,15 @@ jobs:
# Cancelled workflows may have left the ephemeral cluster running
if: always()
run: .github/bin/redshift/delete-aws-redshift.sh
+ - name: Teradata Tests
+ id: tests-teradata
+ env:
+ CLEARSCAPE_TOKEN: ${{ secrets.CLEARSCAPE_TOKEN }}
+ CLEARSCAPE_PASSWORD: ${{ secrets.CLEARSCAPE_PASSWORD }}
+ CLEARSCAPE_REGION: ${{ vars.CLEARSCAPE_REGION }}
+ if: matrix.modules == 'plugin/trino-teradata' && (env.CLEARSCAPE_TOKEN != '' || env.CLEARSCAPE_PASSWORD != '')
+ run: |
+ $MAVEN test ${MAVEN_TEST} -pl :trino-teradata
- name: Sanitize artifact name
if: always()
run: |
@@ -808,6 +846,8 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
build-pt:
+ needs: path-filters
+ if: github.event_name != 'pull_request' || needs.path-filters.outputs.non_docs == 'true'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -821,7 +861,7 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
cache: restore
cleanup-node: true
@@ -852,7 +892,7 @@ jobs:
echo "Impacted plugin features:"
cat impacted-features.log
- name: Product tests artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: product tests and server tarball
path: |
@@ -877,7 +917,6 @@ jobs:
- suite-7-non-generic
- suite-hive-transactional
- suite-azure
- - suite-delta-lake-databricks113
- suite-delta-lake-databricks122
- suite-delta-lake-databricks133
- suite-delta-lake-databricks143
@@ -886,6 +925,7 @@ jobs:
- suite-exasol
- suite-ranger
- suite-gcs
+ - suite-hive4
- suite-clients
- suite-functions
- suite-tpch
@@ -894,6 +934,7 @@ jobs:
- suite-parquet
- suite-oauth2
- suite-ldap
+ - suite-loki
- suite-compatibility
- suite-all-connectors-smoke
- suite-delta-lake-oss
@@ -917,9 +958,6 @@ jobs:
ignore exclusion if: >-
${{ env.CI_SKIP_SECRETS_PRESENCE_CHECKS != '' || secrets.GCP_CREDENTIALS_KEY != '' }}
- - suite: suite-delta-lake-databricks113
- ignore exclusion if: >-
- ${{ env.CI_SKIP_SECRETS_PRESENCE_CHECKS != '' || secrets.DATABRICKS_TOKEN != '' }}
- suite: suite-delta-lake-databricks122
ignore exclusion if: >-
${{ env.CI_SKIP_SECRETS_PRESENCE_CHECKS != '' || secrets.DATABRICKS_TOKEN != '' }}
@@ -979,7 +1017,6 @@ jobs:
AWS_REGION: ""
TRINO_AWS_ACCESS_KEY_ID: ""
TRINO_AWS_SECRET_ACCESS_KEY: ""
- DATABRICKS_113_JDBC_URL: ""
DATABRICKS_122_JDBC_URL: ""
DATABRICKS_133_JDBC_URL: ""
DATABRICKS_143_JDBC_URL: ""
@@ -1024,12 +1061,12 @@ jobs:
github.event.client_payload.pull_request.head.sha == github.event.client_payload.slash_command.args.named.sha &&
format('refs/pull/{0}/head', github.event.client_payload.pull_request.number) || '' }}
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
with:
# The job doesn't build anything, so the ~/.m2/repository cache isn't useful
cache: 'false'
- name: Product tests artifact
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v6
with:
name: product tests and server tarball
- name: Fix artifact permissions
@@ -1052,7 +1089,6 @@ jobs:
AWS_REGION: ${{ vars.TRINO_AWS_REGION }}
TRINO_AWS_ACCESS_KEY_ID: ${{ vars.TRINO_AWS_ACCESS_KEY_ID }}
TRINO_AWS_SECRET_ACCESS_KEY: ${{ secrets.TRINO_AWS_SECRET_ACCESS_KEY }}
- DATABRICKS_113_JDBC_URL: ${{ vars.DATABRICKS_113_JDBC_URL }}
DATABRICKS_122_JDBC_URL: ${{ vars.DATABRICKS_122_JDBC_URL }}
DATABRICKS_133_JDBC_URL: ${{ vars.DATABRICKS_133_JDBC_URL }}
DATABRICKS_143_JDBC_URL: ${{ vars.DATABRICKS_143_JDBC_URL }}
@@ -1093,3 +1129,38 @@ jobs:
check_name: ${{ github.job }} with secrets
conclusion: ${{ job.status }}
github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ build-success:
+ if: ${{ always() }} # if `failure()` would not work for cancellations, `!success()` would not work for skipped jobs
+ runs-on: ubuntu-latest
+ needs:
+ - artifact-checks
+ - build-pt
+ - build-test-matrix
+ - check-commit
+ - check-commits-dispatcher
+ - error-prone-checks
+ - hive-tests
+ - maven-checks
+ - path-filters
+ - pt
+ - test
+ - test-jdbc-compatibility
+ - test-other-modules
+ steps:
+ - name: "Check results"
+ run: |
+ # generated by TestCiWorkflow
+ echo '${{ needs.artifact-checks.result }}' | grep -xE 'success|skipped' || { echo 'Job "artifact-checks" failed' >&2; exit 1; }
+ echo '${{ needs.build-pt.result }}' | grep -xE 'success|skipped' || { echo 'Job "build-pt" failed' >&2; exit 1; }
+ echo '${{ needs.build-test-matrix.result }}' | grep -xE 'success|skipped' || { echo 'Job "build-test-matrix" failed' >&2; exit 1; }
+ echo '${{ needs.check-commit.result }}' | grep -xE 'success|skipped' || { echo 'Job "check-commit" failed' >&2; exit 1; }
+ echo '${{ needs.check-commits-dispatcher.result }}' | grep -xE 'success|skipped' || { echo 'Job "check-commits-dispatcher" failed' >&2; exit 1; }
+ echo '${{ needs.error-prone-checks.result }}' | grep -xE 'success|skipped' || { echo 'Job "error-prone-checks" failed' >&2; exit 1; }
+ echo '${{ needs.hive-tests.result }}' | grep -xE 'success|skipped' || { echo 'Job "hive-tests" failed' >&2; exit 1; }
+ echo '${{ needs.maven-checks.result }}' | grep -xE 'success|skipped' || { echo 'Job "maven-checks" failed' >&2; exit 1; }
+ echo '${{ needs.path-filters.result }}' | grep -xE 'success|skipped' || { echo 'Job "path-filters" failed' >&2; exit 1; }
+ echo '${{ needs.pt.result }}' | grep -xE 'success|skipped' || { echo 'Job "pt" failed' >&2; exit 1; }
+ echo '${{ needs.test.result }}' | grep -xE 'success|skipped' || { echo 'Job "test" failed' >&2; exit 1; }
+ echo '${{ needs.test-jdbc-compatibility.result }}' | grep -xE 'success|skipped' || { echo 'Job "test-jdbc-compatibility" failed' >&2; exit 1; }
+ echo '${{ needs.test-other-modules.result }}' | grep -xE 'success|skipped' || { echo 'Job "test-other-modules" failed' >&2; exit 1; }
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
index 8a3d99a6d733..21265c5b4f78 100644
--- a/.github/workflows/cleanup.yml
+++ b/.github/workflows/cleanup.yml
@@ -18,5 +18,5 @@ jobs:
# Cancel workflow when PR closed. https://github.com/styfle/cancel-workflow-action#advanced-ignore-sha
ignore_sha: true
# Note: workflow_id can be a Workflow ID (number) or Workflow File Name (string) or a comma-separated list of those.
- workflow_id: "ci.yml,docs.yml"
+ workflow_id: "ci.yml"
access_token: ${{ github.token }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
deleted file mode 100644
index 7f7e9d52b53a..000000000000
--- a/.github/workflows/docs.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-name: docs
-
-on:
- pull_request:
- paths:
- - 'docs/**'
-
-defaults:
- run:
- shell: bash --noprofile --norc -euo pipefail {0}
-
-env:
- # An envar that signals to tests we are executing in the CI environment
- CONTINUOUS_INTEGRATION: true
- # allow overriding Maven command
- MAVEN: ./mvnw
- # maven.wagon.rto is in millis, defaults to 30m
- MAVEN_OPTS: "-Xmx512M -XX:+ExitOnOutOfMemoryError -Dmaven.wagon.rto=60000"
- MAVEN_INSTALL_OPTS: "-Xmx2G -XX:+ExitOnOutOfMemoryError -Dmaven.wagon.rto=60000"
- MAVEN_FAST_INSTALL: "-B --strict-checksums -V --quiet -T 1C -DskipTests -Dair.check.skip-all"
- MAVEN_TEST: "-B --strict-checksums -Dair.check.skip-all --fail-at-end"
- RETRY: .github/bin/retry
-
-# Cancel previous PR builds.
-concurrency:
- # Cancel all workflow runs except latest within a concurrency group. This is achieved by defining a concurrency group for the PR.
- # Non-PR builds have singleton concurrency groups.
- group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.number || github.sha }}
- cancel-in-progress: true
-
-jobs:
- path-filters:
- runs-on: ubuntu-latest
- outputs:
- docs: ${{ steps.filter.outputs.docs }}
- other: ${{ steps.filter.outputs.other }}
- steps:
- - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
- id: filter
- with:
- filters: |
- docs: 'docs/**'
- other: '!docs/**'
-
- docs-checks:
- needs: path-filters
- if: ${{ needs.path-filters.outputs.docs == 'true' && needs.path-filters.outputs.other == 'false' }}
- runs-on: ubuntu-latest
- timeout-minutes: 45
- steps:
- - uses: actions/checkout@v5
- - uses: ./.github/actions/setup
- timeout-minutes: 10
- - name: Maven Checks
- run: |
- export MAVEN_OPTS="${MAVEN_INSTALL_OPTS}"
- $RETRY $MAVEN install -B --strict-checksums -V -T 1C -DskipTests -P ci -am -pl ':trino-docs'
- - name: Clean local Maven repo
- # Avoid creating a cache entry because this job doesn't download all dependencies
- if: steps.cache.outputs.cache-hit != 'true'
- run: rm -rf ~/.m2/repository
-
- test-docs:
- needs: path-filters
- if: ${{ needs.path-filters.outputs.docs == 'true' && needs.path-filters.outputs.other == 'false' && !contains(github.event.pull_request.labels.*.name, 'release-notes') }}
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- modules:
- - ":trino-main"
- - ":trino-plugin-toolkit"
- - ":trino-resource-group-managers"
- - ":trino-tests"
- timeout-minutes: 60
- steps:
- - uses: actions/checkout@v5
- with:
- fetch-depth: 0 # checkout all commits, as the build result depends on `git describe` equivalent
- - uses: ./.github/actions/setup
- timeout-minutes: 10
- - name: Maven Install
- run: |
- export MAVEN_OPTS="${MAVEN_INSTALL_OPTS}"
- $RETRY $MAVEN install ${MAVEN_FAST_INSTALL} -am -pl $(echo '${{ matrix.modules }}' | cut -d' ' -f1)
- - name: Maven Tests
- id: tests
- run: $MAVEN test ${MAVEN_TEST} -pl ${{ matrix.modules }}
- - name: Sanitize artifact name
- if: always()
- run: |
- # Generate a valid artifact name and make it available to next steps as
- # an environment variable ARTIFACT_NAME
- # ", :, <, >, |, *, ?, \, / are not allowed in artifact names but we only use : so we remove it
- name=$(echo -n "${{ matrix.modules }}" | sed -e 's/[:]//g')
- echo "ARTIFACT_NAME=$name" >> $GITHUB_ENV
- - name: Upload test results
- uses: ./.github/actions/process-test-results
- if: always()
- with:
- artifact-name: ${{ env.ARTIFACT_NAME }}
- has-failed-tests: ${{ steps.tests.outcome == 'failure' }}
- - name: Clean local Maven repo
- # Avoid creating a cache entry because this job doesn't download all dependencies
- if: steps.cache.outputs.cache-hit != 'true'
- run: rm -rf ~/.m2/repository
diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml
index 5ff6c672585d..31fad28c3129 100644
--- a/.github/workflows/milestone.yml
+++ b/.github/workflows/milestone.yml
@@ -16,11 +16,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- uses: ./.github/actions/setup
- timeout-minutes: 10
+ timeout-minutes: 15
- name: Get milestone from pom.xml
run: |
.github/bin/retry ./mvnw -v
- MILESTONE_NUMBER="$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout | cut -d- -f1)"
+ MILESTONE_NUMBER="$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout --raw-streams | cut -d- -f1)"
echo "Setting PR milestone to ${MILESTONE_NUMBER}"
echo "MILESTONE_NUMBER=${MILESTONE_NUMBER}" >> $GITHUB_ENV
- name: Set milestone to PR
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 04d4ed31adf4..9efd3f3c368e 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'trinodb/trino'
steps:
- - uses: actions/stale@v10.0.0
+ - uses: actions/stale@v10.1.0
with:
stale-pr-message: 'This pull request has gone a while without any activity. Ask for help on #core-dev on Trino slack.'
days-before-pr-stale: 21
diff --git a/README.md b/README.md
index 4e8a6b7dda48..9263d3d60bee 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
io.trino
trino-root
- 478-SNAPSHOT
+ 479-SNAPSHOT
../../pom.xml
diff --git a/client/trino-client/pom.xml b/client/trino-client/pom.xml
index e0bdce83ecf7..e4facbb27cb8 100644
--- a/client/trino-client/pom.xml
+++ b/client/trino-client/pom.xml
@@ -5,7 +5,7 @@
io.trino
trino-root
- 478-SNAPSHOT
+ 479-SNAPSHOT
../../pom.xml
diff --git a/client/trino-client/src/main/java/io/trino/client/DisallowLocalRedirectInterceptor.java b/client/trino-client/src/main/java/io/trino/client/DisallowLocalRedirectInterceptor.java
new file mode 100644
index 000000000000..a40dce4c870a
--- /dev/null
+++ b/client/trino-client/src/main/java/io/trino/client/DisallowLocalRedirectInterceptor.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed 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.client;
+
+import okhttp3.Interceptor;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+
+import static java.lang.String.format;
+
+public class DisallowLocalRedirectInterceptor
+ implements Interceptor
+{
+ public DisallowLocalRedirectInterceptor() {}
+
+ @Override
+ public Response intercept(Chain chain)
+ throws IOException
+ {
+ Response response = chain.proceed(chain.request());
+ if (response.isRedirect()) {
+ String location = response.header("Location");
+ if (!redirectAllowed(location)) {
+ throw new ClientException(format("Following redirect to '%s' is disallowed", location));
+ }
+ }
+ return response;
+ }
+
+ boolean redirectAllowed(String location)
+ {
+ if (location == null) {
+ return true;
+ }
+
+ try {
+ String host = new URI(location).getHost();
+ if (host == null) {
+ return true;
+ }
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+ for (InetAddress address : addresses) {
+ if (isLocalAddress(address)) {
+ return false;
+ }
+ }
+ }
+ catch (URISyntaxException | UnknownHostException ignored) {
+ // This will fail later anyway
+ }
+ return true;
+ }
+
+ static boolean isLocalAddress(InetAddress addr)
+ {
+ return addr.isAnyLocalAddress() ||
+ addr.isLoopbackAddress() ||
+ addr.isLinkLocalAddress() ||
+ addr.isSiteLocalAddress();
+ }
+}
diff --git a/client/trino-client/src/main/java/io/trino/client/OkHttpSegmentLoader.java b/client/trino-client/src/main/java/io/trino/client/OkHttpSegmentLoader.java
index 4a37e298c103..79d9afd5f328 100644
--- a/client/trino-client/src/main/java/io/trino/client/OkHttpSegmentLoader.java
+++ b/client/trino-client/src/main/java/io/trino/client/OkHttpSegmentLoader.java
@@ -61,10 +61,6 @@ public InputStream load(SpooledSegment segment)
.build();
Response response = callFactory.newCall(request).execute();
- if (response.body() == null) {
- throw new IOException("Could not open segment for streaming, got empty body");
- }
-
if (response.isSuccessful()) {
return response.body().byteStream();
}
diff --git a/client/trino-client/src/main/java/io/trino/client/ProtocolHeaders.java b/client/trino-client/src/main/java/io/trino/client/ProtocolHeaders.java
index 69e19da4184f..015ea8953981 100644
--- a/client/trino-client/src/main/java/io/trino/client/ProtocolHeaders.java
+++ b/client/trino-client/src/main/java/io/trino/client/ProtocolHeaders.java
@@ -17,6 +17,40 @@
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_CATALOG;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_CLIENT_CAPABILITIES;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_CLIENT_INFO;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_CLIENT_TAGS;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_EXTRA_CREDENTIAL;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_LANGUAGE;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_ORIGINAL_ROLES;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_ORIGINAL_USER;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_PATH;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_PREPARED_STATEMENT;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_QUERY_DATA_ENCODING;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_RESOURCE_ESTIMATE;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_ROLE;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_SCHEMA;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_SESSION;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_SOURCE;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_TIME_ZONE;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_TRACE_TOKEN;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_TRANSACTION_ID;
+import static io.trino.client.ProtocolHeaders.Headers.REQUEST_USER;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_ADDED_PREPARE;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_CLEAR_SESSION;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_CLEAR_TRANSACTION_ID;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_DEALLOCATED_PREPARE;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_QUERY_DATA_ENCODING;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_RESET_AUTHORIZATION_USER;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_AUTHORIZATION_USER;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_CATALOG;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_ORIGINAL_ROLES;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_PATH;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_ROLE;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_SCHEMA;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_SET_SESSION;
+import static io.trino.client.ProtocolHeaders.Headers.RESPONSE_STARTED_TRANSACTION_ID;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
@@ -24,6 +58,56 @@ public final class ProtocolHeaders
{
public static final ProtocolHeaders TRINO_HEADERS = new ProtocolHeaders("Trino");
+ enum Headers
+ {
+ REQUEST_USER("User"),
+ REQUEST_ORIGINAL_USER("Original-User"),
+ REQUEST_ORIGINAL_ROLES("Original-Roles"),
+ REQUEST_SOURCE("Source"),
+ REQUEST_CATALOG("Catalog"),
+ REQUEST_SCHEMA("Schema"),
+ REQUEST_PATH("Path"),
+ REQUEST_TIME_ZONE("Time-Zone"),
+ REQUEST_LANGUAGE("Language"),
+ REQUEST_TRACE_TOKEN("Trace-Token"),
+ REQUEST_SESSION("Session"),
+ REQUEST_ROLE("Role"),
+ REQUEST_PREPARED_STATEMENT("Prepared-Statement"),
+ REQUEST_TRANSACTION_ID("Transaction-Id"),
+ REQUEST_CLIENT_INFO("Client-Info"),
+ REQUEST_CLIENT_TAGS("Client-Tags"),
+ REQUEST_CLIENT_CAPABILITIES("Client-Capabilities"),
+ REQUEST_RESOURCE_ESTIMATE("Resource-Estimate"),
+ REQUEST_EXTRA_CREDENTIAL("Extra-Credential"),
+ REQUEST_QUERY_DATA_ENCODING("Query-Data-Encoding"),
+ RESPONSE_SET_CATALOG("Set-Catalog"),
+ RESPONSE_SET_SCHEMA("Set-Schema"),
+ RESPONSE_SET_PATH("Set-Path"),
+ RESPONSE_SET_SESSION("Set-Session"),
+ RESPONSE_CLEAR_SESSION("Clear-Session"),
+ RESPONSE_SET_ROLE("Set-Role"),
+ RESPONSE_SET_ORIGINAL_ROLES("Set-Original-Roles"),
+ RESPONSE_QUERY_DATA_ENCODING("Query-Data-Encoding"),
+ RESPONSE_ADDED_PREPARE("Added-Prepare"),
+ RESPONSE_DEALLOCATED_PREPARE("Deallocated-Prepare"),
+ RESPONSE_STARTED_TRANSACTION_ID("Started-Transaction-Id"),
+ RESPONSE_CLEAR_TRANSACTION_ID("Clear-Transaction-Id"),
+ RESPONSE_SET_AUTHORIZATION_USER("Set-Authorization-User"),
+ RESPONSE_RESET_AUTHORIZATION_USER("Reset-Authorization-User");
+
+ private final String headerName;
+
+ Headers(final String headerName)
+ {
+ this.headerName = requireNonNull(headerName, "headerName is null");
+ }
+
+ public String withProtocolName(String protocolName)
+ {
+ return "X-" + protocolName + "-" + headerName;
+ }
+ }
+
private final String name;
private final String requestUser;
private final String requestOriginalUser;
@@ -74,41 +158,40 @@ private ProtocolHeaders(String name)
requireNonNull(name, "name is null");
checkArgument(!name.isEmpty(), "name is empty");
this.name = name;
- String prefix = "X-" + name + "-";
- requestUser = prefix + "User";
- requestOriginalUser = prefix + "Original-User";
- requestOriginalRole = prefix + "Original-Roles";
- requestSource = prefix + "Source";
- requestCatalog = prefix + "Catalog";
- requestSchema = prefix + "Schema";
- requestPath = prefix + "Path";
- requestTimeZone = prefix + "Time-Zone";
- requestLanguage = prefix + "Language";
- requestTraceToken = prefix + "Trace-Token";
- requestSession = prefix + "Session";
- requestRole = prefix + "Role";
- requestPreparedStatement = prefix + "Prepared-Statement";
- requestTransactionId = prefix + "Transaction-Id";
- requestClientInfo = prefix + "Client-Info";
- requestClientTags = prefix + "Client-Tags";
- requestClientCapabilities = prefix + "Client-Capabilities";
- requestResourceEstimate = prefix + "Resource-Estimate";
- requestExtraCredential = prefix + "Extra-Credential";
- requestQueryDataEncoding = prefix + "Query-Data-Encoding";
- responseSetCatalog = prefix + "Set-Catalog";
- responseSetSchema = prefix + "Set-Schema";
- responseSetPath = prefix + "Set-Path";
- responseSetSession = prefix + "Set-Session";
- responseClearSession = prefix + "Clear-Session";
- responseSetRole = prefix + "Set-Role";
- responseQueryDataEncoding = prefix + "Query-Data-Encoding";
- responseAddedPrepare = prefix + "Added-Prepare";
- responseDeallocatedPrepare = prefix + "Deallocated-Prepare";
- responseStartedTransactionId = prefix + "Started-Transaction-Id";
- responseClearTransactionId = prefix + "Clear-Transaction-Id";
- responseSetAuthorizationUser = prefix + "Set-Authorization-User";
- responseResetAuthorizationUser = prefix + "Reset-Authorization-User";
- responseOriginalRole = prefix + "Set-Original-Roles";
+ requestUser = REQUEST_USER.withProtocolName(name);
+ requestOriginalUser = REQUEST_ORIGINAL_USER.withProtocolName(name);
+ requestOriginalRole = REQUEST_ORIGINAL_ROLES.withProtocolName(name);
+ requestSource = REQUEST_SOURCE.withProtocolName(name);
+ requestCatalog = REQUEST_CATALOG.withProtocolName(name);
+ requestSchema = REQUEST_SCHEMA.withProtocolName(name);
+ requestPath = REQUEST_PATH.withProtocolName(name);
+ requestTimeZone = REQUEST_TIME_ZONE.withProtocolName(name);
+ requestLanguage = REQUEST_LANGUAGE.withProtocolName(name);
+ requestTraceToken = REQUEST_TRACE_TOKEN.withProtocolName(name);
+ requestSession = REQUEST_SESSION.withProtocolName(name);
+ requestRole = REQUEST_ROLE.withProtocolName(name);
+ requestPreparedStatement = REQUEST_PREPARED_STATEMENT.withProtocolName(name);
+ requestTransactionId = REQUEST_TRANSACTION_ID.withProtocolName(name);
+ requestClientInfo = REQUEST_CLIENT_INFO.withProtocolName(name);
+ requestClientTags = REQUEST_CLIENT_TAGS.withProtocolName(name);
+ requestClientCapabilities = REQUEST_CLIENT_CAPABILITIES.withProtocolName(name);
+ requestResourceEstimate = REQUEST_RESOURCE_ESTIMATE.withProtocolName(name);
+ requestExtraCredential = REQUEST_EXTRA_CREDENTIAL.withProtocolName(name);
+ requestQueryDataEncoding = REQUEST_QUERY_DATA_ENCODING.withProtocolName(name);
+ responseSetCatalog = RESPONSE_SET_CATALOG.withProtocolName(name);
+ responseSetSchema = RESPONSE_SET_SCHEMA.withProtocolName(name);
+ responseSetPath = RESPONSE_SET_PATH.withProtocolName(name);
+ responseSetSession = RESPONSE_SET_SESSION.withProtocolName(name);
+ responseClearSession = RESPONSE_CLEAR_SESSION.withProtocolName(name);
+ responseSetRole = RESPONSE_SET_ROLE.withProtocolName(name);
+ responseQueryDataEncoding = RESPONSE_QUERY_DATA_ENCODING.withProtocolName(name);
+ responseAddedPrepare = RESPONSE_ADDED_PREPARE.withProtocolName(name);
+ responseDeallocatedPrepare = RESPONSE_DEALLOCATED_PREPARE.withProtocolName(name);
+ responseStartedTransactionId = RESPONSE_STARTED_TRANSACTION_ID.withProtocolName(name);
+ responseClearTransactionId = RESPONSE_CLEAR_TRANSACTION_ID.withProtocolName(name);
+ responseSetAuthorizationUser = RESPONSE_SET_AUTHORIZATION_USER.withProtocolName(name);
+ responseResetAuthorizationUser = RESPONSE_RESET_AUTHORIZATION_USER.withProtocolName(name);
+ responseOriginalRole = RESPONSE_SET_ORIGINAL_ROLES.withProtocolName(name);
}
public String getProtocolName()
diff --git a/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java b/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java
index c8c1b67b5f95..4da7f640c16d 100644
--- a/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java
+++ b/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java
@@ -116,6 +116,7 @@ enum SslVerificationMode
public static final ConnectionProperty> RESOURCE_ESTIMATES = new ResourceEstimates();
public static final ConnectionProperty> SQL_PATH = new SqlPath();
public static final ConnectionProperty VALIDATE_CONNECTION = new ValidateConnection();
+ public static final ConnectionProperty DISALLOW_LOCAL_REDIRECT = new LocalRedirectDisallowed();
private static final Set> ALL_PROPERTIES = ImmutableSet.>builder()
// Keep sorted
@@ -128,6 +129,7 @@ enum SslVerificationMode
.add(CLIENT_INFO)
.add(CLIENT_TAGS)
.add(DISABLE_COMPRESSION)
+ .add(DISALLOW_LOCAL_REDIRECT)
.add(DNS_RESOLVER)
.add(DNS_RESOLVER_CONTEXT)
.add(ENCODING)
@@ -958,6 +960,15 @@ public AssumeNullCatalogMeansCurrentCatalog()
}
}
+ private static class LocalRedirectDisallowed
+ extends AbstractConnectionProperty
+ {
+ public LocalRedirectDisallowed()
+ {
+ super(PropertyName.DISALLOW_LOCAL_REDIRECT, Optional.of(false), NOT_REQUIRED, ALLOWED, BOOLEAN_CONVERTER);
+ }
+ }
+
private static class MapPropertyParser
{
private static final CharMatcher PRINTABLE_ASCII = CharMatcher.inRange((char) 0x21, (char) 0x7E);
diff --git a/client/trino-client/src/main/java/io/trino/client/uri/HttpClientFactory.java b/client/trino-client/src/main/java/io/trino/client/uri/HttpClientFactory.java
index 796a42b40605..b882792c146d 100644
--- a/client/trino-client/src/main/java/io/trino/client/uri/HttpClientFactory.java
+++ b/client/trino-client/src/main/java/io/trino/client/uri/HttpClientFactory.java
@@ -14,6 +14,7 @@
package io.trino.client.uri;
import io.trino.client.ClientException;
+import io.trino.client.DisallowLocalRedirectInterceptor;
import io.trino.client.DnsResolver;
import io.trino.client.auth.external.CompositeRedirectHandler;
import io.trino.client.auth.external.ExternalAuthenticator;
@@ -125,6 +126,10 @@ public static OkHttpClient.Builder unauthenticatedClientBuilder(TrinoUri uri, St
setupTimeouts(builder, toIntExact(uri.getTimeout().toMillis()), TimeUnit.MILLISECONDS);
setupHttpLogging(builder, uri.getHttpLoggingLevel());
+ if (uri.isLocalRedirectDisallowed()) {
+ builder.addNetworkInterceptor(new DisallowLocalRedirectInterceptor());
+ }
+
if (uri.isUseSecureConnection()) {
ConnectionProperties.SslVerificationMode sslVerificationMode = uri.getSslVerification();
if (sslVerificationMode.equals(FULL) || sslVerificationMode.equals(CA)) {
diff --git a/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java b/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java
index b0f13b3ac096..1a7de8011bb3 100644
--- a/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java
+++ b/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java
@@ -76,6 +76,7 @@ public enum PropertyName
TIMEZONE("timezone"),
TRACE_TOKEN("traceToken"),
USER("user"),
+ DISALLOW_LOCAL_REDIRECT("disallowLocalRedirect"),
VALIDATE_CONNECTION("validateConnection");
private final String key;
diff --git a/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java b/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java
index 8b10dbe76586..15e4e9b3f0b5 100644
--- a/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java
+++ b/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java
@@ -51,6 +51,7 @@
import static io.trino.client.uri.ConnectionProperties.CLIENT_INFO;
import static io.trino.client.uri.ConnectionProperties.CLIENT_TAGS;
import static io.trino.client.uri.ConnectionProperties.DISABLE_COMPRESSION;
+import static io.trino.client.uri.ConnectionProperties.DISALLOW_LOCAL_REDIRECT;
import static io.trino.client.uri.ConnectionProperties.DNS_RESOLVER;
import static io.trino.client.uri.ConnectionProperties.DNS_RESOLVER_CONTEXT;
import static io.trino.client.uri.ConnectionProperties.ENCODING;
@@ -423,6 +424,11 @@ public boolean isCompressionDisabled()
return resolveWithDefault(DISABLE_COMPRESSION, false);
}
+ public boolean isLocalRedirectDisallowed()
+ {
+ return resolveWithDefault(DISALLOW_LOCAL_REDIRECT, false);
+ }
+
public Optional getEncoding()
{
Optional encodings = resolveOptional(ENCODING);
@@ -1058,6 +1064,11 @@ public Builder setValidateConnection(boolean value)
return setProperty(VALIDATE_CONNECTION, value);
}
+ public Builder setDisallowLocalRedirect(boolean value)
+ {
+ return setProperty(DISALLOW_LOCAL_REDIRECT, value);
+ }
+
Builder setProperty(ConnectionProperty connectionProperty, T value)
{
properties.put(connectionProperty.getKey(), connectionProperty.encodeValue(value));
diff --git a/client/trino-client/src/test/java/io/trino/client/TestDisallowLocalRedirectInterceptor.java b/client/trino-client/src/test/java/io/trino/client/TestDisallowLocalRedirectInterceptor.java
new file mode 100644
index 000000000000..ea27d38d983e
--- /dev/null
+++ b/client/trino-client/src/test/java/io/trino/client/TestDisallowLocalRedirectInterceptor.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed 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.client;
+
+import okhttp3.Interceptor;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class TestDisallowLocalRedirectInterceptor
+{
+ private static final String BASE_URL = "https://example.com";
+
+ @Test
+ public void testRedirectValidation()
+ throws IOException
+ {
+ DisallowLocalRedirectInterceptor redirector = new DisallowLocalRedirectInterceptor();
+
+ // Valid external URIs
+ assertThat(redirector.intercept(chainWithRedirectLocation("https://www.example.com")))
+ .isNotNull();
+
+ assertThat(redirector.intercept(chainWithRedirectLocation("https://api.github.com")))
+ .isNotNull();
+
+ // Invalid URI
+ assertThat(redirector.intercept(chainWithRedirectLocation("not a valid uri")))
+ .isNotNull();
+
+ // Unresolvable host
+ assertThat(redirector.intercept(chainWithRedirectLocation("https://nonexistent.example.invalid")))
+ .isNotNull();
+
+ // Local URIs
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("https://127.0.0.1")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'https://127.0.0.1' is disallowed");
+
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("https://localhost")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'https://localhost' is disallowed");
+
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("http://192.168.1.1")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'http://192.168.1.1' is disallowed");
+
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("https://0.0.0.0")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'https://0.0.0.0' is disallowed");
+
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("https://172.16.0.1/uri")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'https://172.16.0.1/uri' is disallowed");
+
+ assertThatThrownBy(() -> redirector.intercept(chainWithRedirectLocation("https://169.254.169.254")))
+ .isInstanceOf(ClientException.class)
+ .hasMessage("Following redirect to 'https://169.254.169.254' is disallowed");
+ }
+
+ private static Interceptor.Chain chainWithRedirectLocation(String location)
+ {
+ return new TestingInterceptorChain(new Response.Builder()
+ .request(new Request.Builder().url(BASE_URL).build())
+ .protocol(Protocol.HTTP_1_1)
+ .code(302)
+ .message("Found")
+ .header("Location", location)
+ .build());
+ }
+
+ private static class TestingInterceptorChain
+ implements Interceptor.Chain
+ {
+ private final Response response;
+
+ TestingInterceptorChain(Response response)
+ {
+ this.response = response;
+ }
+
+ @Override
+ public Request request()
+ {
+ return response.request();
+ }
+
+ @Override
+ public Response proceed(Request request)
+ {
+ return response;
+ }
+
+ @Override
+ public int connectTimeoutMillis()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Interceptor.Chain withConnectTimeout(int timeout, java.util.concurrent.TimeUnit unit)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int readTimeoutMillis()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Interceptor.Chain withReadTimeout(int timeout, java.util.concurrent.TimeUnit unit)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int writeTimeoutMillis()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Interceptor.Chain withWriteTimeout(int timeout, java.util.concurrent.TimeUnit unit)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public okhttp3.Connection connection()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public okhttp3.Call call()
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/client/trino-jdbc/pom.xml b/client/trino-jdbc/pom.xml
index dec7bd25e66a..c2ee67c1e9cf 100644
--- a/client/trino-jdbc/pom.xml
+++ b/client/trino-jdbc/pom.xml
@@ -5,7 +5,7 @@
io.trino
trino-root
- 478-SNAPSHOT
+ 479-SNAPSHOT
../../pom.xml
@@ -345,19 +345,19 @@
org.testcontainers
- oracle-xe
+ testcontainers
test
org.testcontainers
- postgresql
+ testcontainers-oracle-free
test
org.testcontainers
- testcontainers
+ testcontainers-postgresql
test
@@ -420,6 +420,21 @@
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+
+
+
+ com.amazonaws:*:*
+
+
+
+
+
+
org.apache.maven.plugins
maven-surefire-plugin
diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/AsyncResultIterator.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/AsyncResultIterator.java
index 24fa9d1ebc9c..cf72441ebb25 100644
--- a/client/trino-jdbc/src/main/java/io/trino/jdbc/AsyncResultIterator.java
+++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/AsyncResultIterator.java
@@ -75,7 +75,7 @@ public class AsyncResultIterator
warningsManager.addWarnings(results.getWarnings());
for (List