diff --git a/.github/ISSUE_TEMPLATE/code-contrib-task.yml b/.github/ISSUE_TEMPLATE/code-contrib-task.yml deleted file mode 100644 index 3191e4fe48d..00000000000 --- a/.github/ISSUE_TEMPLATE/code-contrib-task.yml +++ /dev/null @@ -1,115 +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. -# - -# -# This is a dedicated issue template for 2023 Kyuubi Code Contribution Program, all proposed -# tasks will be listed at https://github.com/orgs/apache/projects/296 after approval -# -name: 2023 Kyuubi Code Contribution Task -title: "[TASK][] " -description: Propose a task for 2023 Kyuubi Code Contribution Program -labels: [ "hacktoberfest" ] -body: - - type: markdown - attributes: - value: | - You are very welcome to propose new task for 2023 Kyuubi Code Contribution Program. - Your brilliant ideas keep Apache Kyuubi evolving. - Please replace the placeholder `` in the issue title with one of the following options: - - TRIVIAL - it's usually for new contributors to learn the contributor process, e.g. how to cut branch, - how to use GitHub to send PR, how to response with reviewers, the contributor should not stay at this - stage too long. - - EASY - tasks like minor bugs, or simple features without requirements of knowledge for whole Kyuubi - architecture. - - MEDIUM - tasks typical requires that contributors have knowledge on one or more Kyuubi components, - normally, unit tests and integration tests is also required to verify the implementations. - - CHALLENGE - tasks requires that contributors have deep knowledge on one or more Kyuubi components, - have good logical thinking and the ability to solve complex problems, be proficient in programming - skills or algorithms - - - type: checkboxes - attributes: - label: Code of Conduct - description: The Code of Conduct helps create a safe space for everyone. We require that everyone agrees to it. - options: - - label: > - I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct) - required: true - - - type: checkboxes - attributes: - label: Search before creating - options: - - label: > - I have searched in the [task list](https://github.com/orgs/apache/projects/296) and found no similar - tasks. - required: true - - - type: checkboxes - attributes: - label: Mentor - description: Mentor is required for MEDIUM and CHALLENGE tasks, to guide contributors to complete the task. - options: - - label: > - I have sufficient knowledge and experience of this task, and I volunteer to be the mentor of this task - to guide contributors to complete the task. - required: false - - - type: textarea - attributes: - label: Skill requirements - description: Which stills are required for contributors who want to take this task? - placeholder: | - e.g. - - Basic knowledge on Scala Programing Language - - Familiar with Apache Maven, Docker and GitHub Action - - Basic knowledge on network programing and Apache Thrift RPC framework - - Familiar with Apache Spark - - ... - validations: - required: true - - - type: textarea - attributes: - label: Background and Goals - description: What's the current problem, and what's the final status should be after the task is completed? - placeholder: > - Please describe the background and your goal for requesting this task. - validations: - required: true - - - type: textarea - attributes: - label: Implementation steps - description: How could it be implemented? - placeholder: > - Please list the implementation steps in as much detail as possible so that contributors who meet - the skill requirements could complete the task quickly and independently. - validations: - required: true - - - type: textarea - attributes: - label: Additional context - placeholder: > - Anything else that related to this task that the contributors need to know. - validations: - required: false - - - type: markdown - attributes: - value: "Thanks for taking the time to fill out this task form!" diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 3cab99d1fe8..6de5d0adccd 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -1,32 +1,56 @@ - + -Here are some tips for you: - 1. If this is your first time, please read our contributor guidelines: https://kyuubi.readthedocs.io/en/latest/community/CONTRIBUTING.html - 2. If the PR is related to an issue in https://github.com/apache/kyuubi/issues, add '[KYUUBI #XXXX]' in your PR title, e.g., '[KYUUBI #XXXX] Your PR title ...'. - 3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., '[WIP][KYUUBI #XXXX] Your PR title ...'. ---> +This pull request fixes # -### _Why are the changes needed?_ - +## Describe Your Solution ๐Ÿ”ง +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -### _How was this patch tested?_ -- [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible -- [ ] Add screenshots for manual tests if appropriate +## Types of changes :bookmark: + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) -- [ ] [Run test](https://kyuubi.readthedocs.io/en/master/contributing/code/testing.html#running-tests) locally before make a pull request +## Test Plan ๐Ÿงช +#### Behavior Without This Pull Request :coffin: -### _Was this patch authored or co-authored using generative AI tooling?_ - + +#### Behavior With This Pull Request :tada: + + +#### Related Unit Tests + + +--- + +# Checklists +## ๐Ÿ“ Author Self Checklist + + +- [ ] My code follows the [style guidelines](https://kyuubi.readthedocs.io/en/master/contributing/code/style.html) of this project +- [ ] I have performed a self-review +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] This patch was not authored or co-authored using [Generative Tooling](https://www.apache.org/legal/generative-tooling.html) + +## ๐Ÿ“ Committer Pre-Merge Checklist + +- [ ] Pull request title is okay. +- [ ] No license issues. +- [ ] Milestone correctly set? +- [ ] Test coverage is ok +- [ ] Assignees are selected. +- [ ] Minimum number of approvals +- [ ] No changes are requested + + +**Be nice. Be informative.** diff --git a/.github/labeler.yml b/.github/labeler.yml index ecec1253274..e76dad43902 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -19,109 +19,181 @@ # Pull Request Labeler Github Action Configuration: https://github.com/marketplace/actions/labeler "kind:build": - - ".dockerignore" - - ".rat-excludes" - - ".scalafmt" - - "**/*pom.xml" - - "bin/docker-image-tool.sh" - - "build/**/*" - - "docker/**/*" - - "docs/requirements" - - "kyuubi-assembly/**/*" - - "scalastyle-config.xml" - - any: ["dev/**/*", "!dev/kyuubi-codecov/**/*", "!dev/kyuubi-tpcds/**/*"] + - changed-files: + - any-glob-to-any-file: [ + '.dockerignore', + '.rat-excludes', + '.scalafmt', + '**/*pom.xml', + 'bin/docker-image-tool.sh', + 'build/**/*', + 'docker/**/*', + 'docs/requirements', + 'kyuubi-assembly/**/*', + 'scalastyle-config.xml' + ] + - all-globs-to-any-file: [ + 'dev/**/*', + '!dev/kyuubi-codecov/**/*', + '!dev/kyuubi-tpcds/**/*' + ] "kind:deploy": - - any: ["bin/**/*", "!bin/beeline", "!bin/docker-image-tool.sh"] + - changed-files: + - all-globs-to-any-file: [ + 'bin/**/*', + '!bin/beeline', + '!bin/docker-image-tool.sh' + ] "kind:documentation": - - "*.md" - - "conf/**/*" - - "docs/**/*" - - "readthedocs.yml" + - changed-files: + - any-glob-to-any-file: [ + '*.md', + 'conf/**/*', + 'docs/**/*', + 'readthedocs.yml' + ] "kind:infra": - - ".asf.yaml" - - ".gitattributes" - - ".github/**/*" - - ".gitignore" - - "LICENSE" - - "LICENSE-binary" - - "NOTICE" - - "NOTICE-binary" - - "codecov.yml" - - "dev/kyuubi-codecov/**/*" - - "licenses-binary" + - changed-files: + - any-glob-to-any-file: [ + '.asf.yaml', + '.gitattributes', + '.github/**/*', + '.gitignore', + 'LICENSE', + 'LICENSE-binary', + 'NOTICE', + 'NOTICE-binary', + 'codecov.yml', + 'dev/kyuubi-codecov/**/*', + 'licenses-binary' + ] "module:common": - - "kyuubi-common/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-common/**/*' + ] "module:ctl": - - "bin/beeline" - - "kyuubi-ctl/**/*" - - "kyuubi-hive-beeline/**/*" - - "kyuubi-hive-jdbc/**/*" - - "kyuubi-hive-jdbc-shaded/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'bin/beeline', + 'kyuubi-ctl/**/*', + 'kyuubi-hive-beeline/**/*', + 'kyuubi-hive-jdbc/**/*', + 'kyuubi-hive-jdbc-shaded/**/*' + ] "module:events": - - "kyuubi-events/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-events/**/*' + ] "module:flink": - - "externals/kyuubi-flink-sql-engine/**/*" - - "integration-tests/kyuubi-flink-it/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'externals/kyuubi-flink-sql-engine/**/*', + 'integration-tests/kyuubi-flink-it/**/*' + ] "module:ha": - - "kyuubi-ha/**/*" - - "kyuubi-zookeeper/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-ha/**/*', + 'kyuubi-zookeeper/**/*' + ] "module:hive": - - "bin/beeline" - - "externals/kyuubi-hive-sql-engine/**/*" - - "kyuubi-hive-beeline/**/*" - - "kyuubi-hive-jdbc/**/*" - - "kyuubi-hive-jdbc-shaded/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'bin/beeline', + 'externals/kyuubi-hive-sql-engine/**/*', + 'kyuubi-hive-beeline/**/*', + 'kyuubi-hive-jdbc/**/*', + 'kyuubi-hive-jdbc-shaded/**/*' + ] "module:jdbc": - - "externals/kyuubi-jdbc-engine/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'externals/kyuubi-jdbc-engine/**/*' + ] "module:kubernetes": - - ".dockerignore" - - "bin/docker-image-tool.sh" - - "docker/**/*" - - "integration-tests/kyuubi-kubernetes-it/**/*" - - "tools/spark-block-cleaner/**/*" + - changed-files: + - any-glob-to-any-file: [ + '.dockerignore', + 'bin/docker-image-tool.sh', + 'docker/**/*', + 'integration-tests/kyuubi-kubernetes-it/**/*', + 'tools/spark-block-cleaner/**/*' + ] "module:metrics": - - "kyuubi-metrics/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-metrics/**/*' + ] "module:trino": - - "externals/kyuubi-trino-engine/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'externals/kyuubi-trino-engine/**/*' + ] "module:tpcds": - - "dev/kyuubi-tpcds/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'dev/kyuubi-tpcds/**/*' + ] "module:server": - - "bin/kyuubi" - - "kyuubi-server/src/**/*" - - "kyuubi-server/pom.xml" - - "extension/server/kyuubi-server-plugin/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'bin/kyuubi', + 'kyuubi-server/src/**/*', + 'kyuubi-server/pom.xml', + 'extension/server/kyuubi-server-plugin/**/*' + ] "module:spark": - - "externals/kyuubi-spark-sql-engine/**/*" - - "extensions/spark/**/*" - - "tools/spark-block-cleaner/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'externals/kyuubi-spark-sql-engine/**/*', + 'extensions/spark/**/*', + 'tools/spark-block-cleaner/**/*' + ] "module:extensions": - - "extensions/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'extensions/**/*' + ] "module:rest-client": - - "kyuubi-rest-client/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-rest-client/**/*' + ] "module:integration-tests": - - "integration-tests/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'integration-tests/**/*' + ] "module:authz": - - "extensions/spark/kyuubi-spark-authz/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'extensions/spark/kyuubi-spark-authz/**/*' + ] "module:ui": - - "kyuubi-server/web-ui/**/*" + - changed-files: + - any-glob-to-any-file: [ + 'kyuubi-server/web-ui/**/*' + ] diff --git a/.github/workflows/dep.yml b/.github/workflows/dep.yml index f39e5e6a212..96f49d8d9b8 100644 --- a/.github/workflows/dep.yml +++ b/.github/workflows/dep.yml @@ -26,6 +26,8 @@ on: # when pom or dependency workflow changes - '**/pom.xml' - '.github/workflows/dep.yml' + - 'build/dependency.sh' + - 'dev/dependencyList' concurrency: group: dep-${{ github.head_ref || github.run_id }} @@ -36,9 +38,9 @@ jobs: name: Dependency check runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 55cb6b8b16b..3b2ea90d660 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,8 +31,8 @@ jobs: name: sphinx-build runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' cache: 'pip' diff --git a/.github/workflows/gluten.yml b/.github/workflows/gluten.yml new file mode 100644 index 00000000000..23b4f0d3bbc --- /dev/null +++ b/.github/workflows/gluten.yml @@ -0,0 +1,128 @@ +# +# 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. +# + +name: Gluten CI + +on: + schedule: + - cron: 0 4 * * * + +env: + MVN_OPT: -Dmaven.javadoc.skip=true -Drat.skip=true -Dscalastyle.skip=true -Dspotless.check.skip -Dorg.slf4j.simpleLogger.defaultLogLevel=warn -Pjdbc-shaded,gen-policy -Dmaven.plugin.download.cache.path=/tmp/engine-archives + +jobs: + gluten-build: + name: Build Gluten + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Tune Runner VM + uses: ./.github/actions/tune-runner-vm + - name: Update and Upgrade + run: sudo apt-get update && sudo apt-get upgrade -y + - name: Install dependencies + run: | + sudo apt-get install -y software-properties-common + sudo apt-get install -y libunwind-dev build-essential cmake libssl-dev libre2-dev libcurl4-openssl-dev clang lldb lld libz-dev git ninja-build uuid-dev + - name: Setup JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: 'maven' + check-latest: false + - name: Setup Maven + uses: ./.github/actions/setup-maven + - name: Get gluten cache date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Check gluten cache + id: gluten-cache + uses: actions/cache@v3 + with: + path: gluten/package/target/ + key: gluten_package_${{ steps.date.outputs.date }} + - name: Build gluten project + run: | + if [[ "${{ steps.gluten-cache.outputs.cache-hit }}" != 'true' ]]; then + git clone https://github.com/oap-project/gluten.git + cd gluten + ./dev/buildbundle-veloxbe.sh + fi + - uses: actions/cache@v3 + if: steps.gluten-cache.outputs.cache-hit != 'true' + with: + path: gluten/package/target/ + key: gluten_package_${{ steps.date.outputs.date }} + + gluten-it: + name: Gluten Integration TPC-H/DS Test + needs: gluten-build + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + spark: [ '3.4', '3.3' ] + steps: + - uses: actions/checkout@v4 + - name: Tune Runner VM + uses: ./.github/actions/tune-runner-vm + - name: Update and Upgrade + run: sudo apt-get update && sudo apt-get upgrade -y + - name: Install dependencies + run: | + sudo apt-get install -y software-properties-common + sudo apt-get install -y libunwind-dev build-essential cmake libssl-dev libre2-dev libcurl4-openssl-dev clang lldb lld libz-dev git ninja-build uuid-dev + sudo apt-get install -y libsnappy-dev libthrift-dev libboost-all-dev libgflags-dev libgoogle-glog-dev + - name: Cache Engine Archives + uses: ./.github/actions/cache-engine-archives + - name: Get gluten cache date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Check gluten cache + id: gluten-cache + uses: actions/cache@v3 + with: + path: gluten/package/target/ + key: gluten_package_${{ steps.date.outputs.date }} + - name: Cache Gluten Package + uses: actions/cache@v3 + with: + path: gluten/package/target/ + key: gluten_package + - name: Setup JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: 'maven' + check-latest: false + - name: Setup Maven + uses: ./.github/actions/setup-maven + - name: Run Gluten Integration TPC-H/DS Test + run: | + TEST_MODULES="integration-tests/kyuubi-gluten-it" + ./build/mvn ${MVN_OPT} -pl ${TEST_MODULES} -am clean install -DskipTests -Pgluten-spark-${{ matrix.spark }} + ./build/mvn ${MVN_OPT} -pl ${TEST_MODULES} test -Pgluten-spark-${{ matrix.spark }} \ + -Dmaven.plugin.scalatest.exclude.tags='' -Dtest=none -Dmaven.plugin.scalatest.include.tags='org.apache.kyuubi.tags.GlutenTest' + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: unit-tests-log-spark-${{ matrix.spark }}-gluten + path: | + **/target/unit-tests.log diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c4cad7aef2d..7d6cd5bd217 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -28,7 +28,7 @@ jobs: triage: runs-on: ubuntu-22.04 steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml index 55ef485f8fe..cc1ab623630 100644 --- a/.github/workflows/license.yml +++ b/.github/workflows/license.yml @@ -34,9 +34,9 @@ jobs: name: License runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 1819c4850af..289e32c14b0 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -86,11 +86,11 @@ jobs: env: SPARK_LOCAL_IP: localhost steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -101,14 +101,17 @@ jobs: - name: Setup Maven uses: ./.github/actions/setup-maven - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' - name: Build and test Kyuubi and Spark with maven w/o linters run: | + if [[ "${{ matrix.java }}" == "8" && "${{ matrix.spark }}" == "3.4" && "${{ matrix.spark-archive }}" == "" ]]; then + MVN_OPT="${MVN_OPT} -Pcodecov" + fi TEST_MODULES="dev/kyuubi-codecov" ./build/mvn clean install ${MVN_OPT} -pl ${TEST_MODULES} -am \ - -Pspark-${{ matrix.spark }} -Pspark-authz-hudi-test ${{ matrix.spark-archive }} ${{ matrix.exclude-tags }} + -Pjava-${{ matrix.java }} -Pspark-${{ matrix.spark }} -Pspark-authz-hudi-test ${{ matrix.spark-archive }} ${{ matrix.exclude-tags }} - name: Code coverage if: | matrix.java == 8 && @@ -140,11 +143,11 @@ jobs: spark: - '3.4' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -156,7 +159,7 @@ jobs: uses: ./.github/actions/cache-engine-archives - name: Build on Scala ${{ matrix.scala }} run: | - TEST_MODULES="!externals/kyuubi-flink-sql-engine,!integration-tests/kyuubi-flink-it" + TEST_MODULES="!externals/kyuubi-flink-sql-engine,!integration-tests/kyuubi-flink-it,!integration-tests/kyuubi-gluten-it" ./build/mvn clean install ${MVN_OPT} -pl ${TEST_MODULES} -am \ -Pscala-${{ matrix.scala }} -Pjava-${{ matrix.java }} -Pspark-${{ matrix.spark }} - name: Upload test logs @@ -196,11 +199,11 @@ jobs: flink-archive: '-Dflink.archive.mirror=https://archive.apache.org/dist/flink/flink-1.18.0 -Dflink.archive.name=flink-1.18.0-bin-scala_2.12.tgz' comment: 'verify-on-flink-1.18-binary' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -242,13 +245,21 @@ jobs: matrix: java: - 8 + hive-archive: [ "" ] comment: [ "normal" ] + include: + - java: 8 + hive-archive: '-Dhive.archive.mirror=https://archive.apache.org/dist/hive/hive-2.3.9 -Dhive.archive.name=apache-hive-2.3.9-bin.tar.gz' + comment: 'verify-on-hive-2.3-binary' + - java: 8 + hive-archive: '-Dhive.archive.mirror=https://github.com/pan3793/cdh-hive/releases/download/cdh6.3.2-release -Dhive.archive.name=apache-hive-2.1.1-cdh6.3.2-bin.tar.gz' + comment: 'verify-on-hive-2.1-cdh6-binary' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -261,8 +272,15 @@ jobs: - name: Build and test Hive with maven w/o linters run: | TEST_MODULES="externals/kyuubi-hive-sql-engine,integration-tests/kyuubi-hive-it" - ./build/mvn ${MVN_OPT} -pl ${TEST_MODULES} -am clean install -DskipTests - ./build/mvn ${MVN_OPT} -pl ${TEST_MODULES} test + ./build/mvn ${MVN_OPT} ${{ matrix.hive-archive }} -pl ${TEST_MODULES} -am clean install -DskipTests + # Hive 2.3.9 ships Derby 10.10.2.0, which may fail to boostrap on latest JDK 8 + # https://github.com/apache/hive/pull/4895 + if [[ "${{ matrix.hive-archive }}" == *apache-hive-2.3.9-bin.tar.gz* ]]; then + HIVE_239_LIB="$PWD/externals/kyuubi-download/target/apache-hive-2.3.9-bin/lib" + rm $HIVE_239_LIB/derby-* + wget https://repo1.maven.org/maven2/org/apache/derby/derby/10.14.2.0/derby-10.14.2.0.jar -P $HIVE_239_LIB + fi + ./build/mvn ${MVN_OPT} ${{ matrix.hive-archive }} -pl ${TEST_MODULES} test - name: Upload test logs if: failure() uses: actions/upload-artifact@v3 @@ -283,11 +301,11 @@ jobs: - 11 comment: [ "normal" ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -322,11 +340,11 @@ jobs: - 11 comment: [ "normal" ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} @@ -356,11 +374,11 @@ jobs: env: SPARK_LOCAL_IP: localhost steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -383,16 +401,16 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # https://github.com/docker/build-push-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build Kyuubi Docker Image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: # passthrough CI into build container build-args: | - CI=${CI} + CI=${CI} MVN_ARG=--flink-provided --hive-provided -Dmaven.javadoc.skip=true -Drat.skip=true -Dscalastyle.skip=true -Dspotless.check.skip -DskipTests context: . file: build/Dockerfile @@ -409,8 +427,8 @@ jobs: # https://minikube.sigs.k8s.io/docs/handbook/pushing/#7-loading-directly-to-in-cluster-container-runtime minikube image load apache/kyuubi:latest # pre-install spark into minikube - docker pull apache/spark:3.4.1 - minikube image load apache/spark:3.4.1 + docker pull apache/spark:3.4.2 + minikube image load apache/spark:3.4.2 - name: kubectl pre-check run: | kubectl get nodes @@ -455,7 +473,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Cache Engine Archives uses: ./.github/actions/cache-engine-archives - name: Setup Minikube @@ -502,11 +520,11 @@ jobs: zookeeper: ["3.4", "3.5", "3.6", "3.7" ] comment: [ "normal" ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5ff634da6d8..1ba696bbe6f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -33,11 +33,11 @@ jobs: env: SPARK_LOCAL_IP: localhost steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Tune Runner VM uses: ./.github/actions/tune-runner-vm - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 diff --git a/.github/workflows/publish-snapshot-docker.yml b/.github/workflows/publish-snapshot-docker.yml index 3afccee7aa8..0a73dcc2da5 100644 --- a/.github/workflows/publish-snapshot-docker.yml +++ b/.github/workflows/publish-snapshot-docker.yml @@ -28,18 +28,18 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Kyuubi Docker Image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: # build cache on Github Actions, See: https://docs.docker.com/build/cache/backends/gha/#using-dockerbuild-push-action cache-from: type=gha diff --git a/.github/workflows/publish-snapshot-nexus.yml b/.github/workflows/publish-snapshot-nexus.yml index b4191396b1f..64dd1a690b1 100644 --- a/.github/workflows/publish-snapshot-nexus.yml +++ b/.github/workflows/publish-snapshot-nexus.yml @@ -43,11 +43,11 @@ jobs: profiles: -Pflink-provided,spark-provided,hive-provided,spark-3.4 steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ matrix.branch }} - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d189cd205db..38bb12a4f9a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -27,7 +27,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/stale@v7 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-pr-message: > diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 87823ddbd20..5b8b6a7048d 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -37,11 +37,11 @@ jobs: - '-Pflink-provided,hive-provided,spark-provided,spark-block-cleaner,spark-3.5,spark-3.4,spark-3.3,spark-3.2,tpcds,kubernetes-it' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -50,7 +50,7 @@ jobs: - name: Setup Maven uses: ./.github/actions/setup-maven - name: Setup Python 3 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' cache: 'pip' @@ -92,7 +92,7 @@ jobs: pip install black==$SPOTLESS_BLACK_VERSION build/mvn spotless:check ${{ matrix.profiles }} -Pspotless-python,spark-3.1 - name: setup npm - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Web UI Style with node @@ -114,7 +114,7 @@ jobs: name: Super Linter and Shellcheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Super Linter Checks uses: github/super-linter/slim@v5 env: diff --git a/.github/workflows/web-ui.yml b/.github/workflows/web-ui.yml index 9de7a599d45..ec0a88575b2 100644 --- a/.github/workflows/web-ui.yml +++ b/.github/workflows/web-ui.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-22.04 steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup JDK 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -35,7 +35,7 @@ jobs: echo "NODEJS_VERSION=${NODEJS_VERSION}" >> "$GITHUB_ENV" echo "PNPM_VERSION=${PNPM_VERSION}" >> "$GITHUB_ENV" - name: Setup Nodejs and NPM - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{env.NODEJS_VERSION}} cache: npm diff --git a/.gitignore b/.gitignore index a2f6fb1efe4..dcf808e6752 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ .settings build/apache-maven* build/release/tmp +build/release/*.txt build/scala* build/test target/ diff --git a/LICENSE-binary b/LICENSE-binary index 748842a6191..b225b2c6288 100644 --- a/LICENSE-binary +++ b/LICENSE-binary @@ -506,6 +506,8 @@ is auto-generated by `pnpm licenses list --prod`. โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ postcss โ”‚ MIT โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ proxy-from-env โ”‚ MIT โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ randexp โ”‚ MIT โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ret โ”‚ MIT โ”‚ diff --git a/build/dist b/build/dist index df9498008cb..2ea702b61af 100755 --- a/build/dist +++ b/build/dist @@ -249,6 +249,7 @@ mkdir -p "$DISTDIR/pid" mkdir -p "$DISTDIR/logs" mkdir -p "$DISTDIR/work" mkdir -p "$DISTDIR/jars" +mkdir -p "$DISTDIR/db-scripts" mkdir -p "$DISTDIR/beeline-jars" mkdir -p "$DISTDIR/web-ui" mkdir -p "$DISTDIR/externals/engines/flink" @@ -270,6 +271,9 @@ echo "Build flags: $@" >> "$DISTDIR/RELEASE" # Copy kyuubi server jars cp -r "$KYUUBI_HOME"/kyuubi-assembly/target/scala-$SCALA_VERSION/jars/*.jar "$DISTDIR/jars/" +# Copy kyuubi database scripts +cp -r "$KYUUBI_HOME"/kyuubi-server/src/main/resources/sql/* "$DISTDIR/db-scripts/" + # Copy kyuubi beeline jars cp "$KYUUBI_HOME"/kyuubi-hive-beeline/target/*.jar "$DISTDIR/beeline-jars/" diff --git a/build/release/known_translations b/build/release/known_translations new file mode 100644 index 00000000000..73fd1ad55e3 --- /dev/null +++ b/build/release/known_translations @@ -0,0 +1,60 @@ +# +# 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 is a mapping of names to be translated. +# The format expected on each line should be: - +AngersZhuuuu - Yi Zhu +ASiegeLion - Peiyue Liu +bowenliang123 - Bowen Liang +BruceWong96 - Bruce Wong +CavemanIV - Liang Zhang +cxzl25 - Shaoyun Chen +davidyuan1223 - David Yuan +dependabot[bot] - GitHub Bot +dev-lpq - Pengqi Li +dnskr - Denis Krivenko +edddddy - Yang Du +hadoopkandy - Kang Wang +HaoYang670 - Remzi Yang +huage1994 - Guanhua Li +iodone - Yaodong Zhang +ITzhangqiang - Qiang Zhang +Kiss736921 - Alex Zou +labbomb - Junjie Xu +lightning_L - Tianlin Liao +liunaijie - Naijie Liu +lsm1 - Senmiao Liu +mattshma - Ming Ma +merrily01 - Ruilei Ma +minyk - Drake Youngkun Min +packyan - Deng An +QianyongY - Yong Qian +thomasg19930417 - Xu Guo +turboFei - Fei Wang +ulysses-you - Xiduo You +wangmiao1002 - Miao Wang +wForget - Zhen Wang +Xieming LI - Xieming Li +XorSum - Baokun Han +yabola - Chenliang Lu +Yikf - Kaifei Yi +ymZhao1001 - Yangming Zhao +zhaohehuhu - He Zhao +zhaomin1423 - Min Zhao +zhouyifan279 - Yifan Zhou +zhuyaogai - Yaogai Zhu +zwangsheng - Binjie Yang diff --git a/build/release/release.sh b/build/release/release.sh index 047513dc5e3..49fef9f8b24 100755 --- a/build/release/release.sh +++ b/build/release/release.sh @@ -124,12 +124,17 @@ upload_nexus_staging() { -s "${KYUUBI_DIR}/build/release/asf-settings.xml" \ -pl extensions/spark/kyuubi-extension-spark-3-3 -am - # Spark TPC-DS/TPC-H Connector build with default Spark version (3.4) and Scala 2.13 - ${KYUUBI_DIR}/build/mvn clean deploy -DskipTests -Papache-release,flink-provided,spark-provided,hive-provided,spark-3.4 \ + # Spark Extension Plugin for Spark 3.5 + ${KYUUBI_DIR}/build/mvn clean deploy -DskipTests -Papache-release,flink-provided,spark-provided,hive-provided,spark-3.5 \ + -s "${KYUUBI_DIR}/build/release/asf-settings.xml" \ + -pl extensions/spark/kyuubi-extension-spark-3-5 -am + + # Spark TPC-DS/TPC-H Connector built with default Spark version (3.4) and Scala 2.13 + ${KYUUBI_DIR}/build/mvn clean deploy -DskipTests -Papache-release,flink-provided,spark-provided,hive-provided,spark-3.4,scala-2.13 \ -s "${KYUUBI_DIR}/build/release/asf-settings.xml" \ - -pl extensions/spark/kyuubi-connector-tpcds,extensions/spark/kyuubi-connector-tpch + -pl extensions/spark/kyuubi-spark-connector-tpcds,extensions/spark/kyuubi-spark-connector-tpch -am - # All modules including Spark Extension Plugin and Connectors build with default Spark version (3.4) and default Scala version (2.12) + # All modules including Spark Extension Plugin and Connectors built with default Spark version (3.4) and default Scala version (2.12) ${KYUUBI_DIR}/build/mvn clean deploy -DskipTests -Papache-release,flink-provided,spark-provided,hive-provided,spark-3.4 \ -s "${KYUUBI_DIR}/build/release/asf-settings.xml" } diff --git a/charts/kyuubi/Chart.yaml b/charts/kyuubi/Chart.yaml index 56abc9edc88..2fefab2886d 100644 --- a/charts/kyuubi/Chart.yaml +++ b/charts/kyuubi/Chart.yaml @@ -20,7 +20,7 @@ name: kyuubi description: A Helm chart for Kyuubi server type: application version: 0.1.0 -appVersion: 1.7.3 +appVersion: 1.8.0 home: https://kyuubi.apache.org icon: https://raw.githubusercontent.com/apache/kyuubi/master/docs/imgs/logo.png sources: diff --git a/charts/kyuubi/templates/kyuubi-service.yaml b/charts/kyuubi/templates/kyuubi-service.yaml index 64c8b06ac20..9d9362e86d6 100644 --- a/charts/kyuubi/templates/kyuubi-service.yaml +++ b/charts/kyuubi/templates/kyuubi-service.yaml @@ -37,6 +37,12 @@ spec: {{- end }} selector: {{- include "kyuubi.selectorLabels" $ | nindent 4 }} + {{- if ($frontend.service.sessionAffinity) }} + sessionAffinity: {{ $frontend.service.sessionAffinity }} + {{- end }} + {{- with $frontend.service.sessionAffinityConfig }} + sessionAffinityConfig: {{- toYaml . | nindent 4 }} + {{- end }} --- {{- end }} {{- end }} diff --git a/charts/kyuubi/values.yaml b/charts/kyuubi/values.yaml index faa854b1017..044668040f3 100644 --- a/charts/kyuubi/values.yaml +++ b/charts/kyuubi/values.yaml @@ -85,6 +85,13 @@ server: port: "{{ .Values.server.thriftBinary.port }}" nodePort: ~ annotations: {} + # candidates are ClientIP or None + # https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/ + sessionAffinity: ~ + sessionAffinityConfig: {} + # sessionAffinityConfig: + # clientIP: + # timeoutSeconds: 10800 # Thrift HTTP protocol (HiveServer2 compatible) thriftHttp: @@ -95,6 +102,13 @@ server: port: "{{ .Values.server.thriftHttp.port }}" nodePort: ~ annotations: {} + # candidates are ClientIP or None + # https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/ + sessionAffinity: ~ + sessionAffinityConfig: {} + # sessionAffinityConfig: + # clientIP: + # timeoutSeconds: 10800 # REST API protocol (experimental) rest: @@ -105,6 +119,13 @@ server: port: "{{ .Values.server.rest.port }}" nodePort: ~ annotations: {} + # candidates are ClientIP or None + # https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/ + sessionAffinity: ~ + sessionAffinityConfig: {} + # sessionAffinityConfig: + # clientIP: + # timeoutSeconds: 10800 # MySQL compatible text protocol (experimental) mysql: @@ -115,6 +136,13 @@ server: port: "{{ .Values.server.mysql.port }}" nodePort: ~ annotations: {} + # candidates are ClientIP or None + # https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/ + sessionAffinity: ~ + sessionAffinityConfig: {} + # sessionAffinityConfig: + # clientIP: + # timeoutSeconds: 10800 monitoring: # Exposes metrics in Prometheus format diff --git a/codecov.yml b/codecov.yml index 6267ea38074..1be776f5831 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,4 +16,11 @@ # codecov: - token: b624e642-b0c8-4d45-94a1-a370888435bb + token: 5115fd3e-2ef2-40ed-b012-376a2afdc382 + +coverage: + status: + project: + default: + target: auto # auto compares coverage to the previous base commit + threshold: 2% #this allows a 2% drop from the previous base commit coverage diff --git a/conf/kyuubi-env.sh.template b/conf/kyuubi-env.sh.template index 2b7be6fc89a..2d89d3a5452 100755 --- a/conf/kyuubi-env.sh.template +++ b/conf/kyuubi-env.sh.template @@ -64,5 +64,5 @@ # export HIVE_HADOOP_CLASSPATH=${HADOOP_HOME}/share/hadoop/common/lib/commons-collections-3.2.2.jar:${HADOOP_HOME}/share/hadoop/client/hadoop-client-runtime-3.1.0.jar:${HADOOP_HOME}/share/hadoop/client/hadoop-client-api-3.1.0.jar:${HADOOP_HOME}/share/hadoop/common/lib/htrace-core4-4.1.0-incubating.jar # export HADOOP_CONF_DIR=/usr/ndp/current/mapreduce_client/conf # export YARN_CONF_DIR=/usr/ndp/current/yarn/conf -# export KYUUBI_JAVA_OPTS="-Xmx10g -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=4096 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+UseCondCardMark -XX:MaxDirectMemorySize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -Xloggc:./logs/kyuubi-server-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=5M -XX:NewRatio=3 -XX:MetaspaceSize=512m" -# export KYUUBI_BEELINE_OPTS="-Xmx2g -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=4096 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+UseCondCardMark" +# export KYUUBI_JAVA_OPTS="-Xmx10g -XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=1024m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+UnlockDiagnosticVMOptions -XX:+UseCondCardMark -XX:+UseGCOverheadLimit -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -verbose:gc -Xloggc:./logs/kyuubi-server-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=20M" +# export KYUUBI_BEELINE_OPTS="-Xmx2g -XX:+UseG1GC -XX:+UnlockDiagnosticVMOptions -XX:+UseCondCardMark" diff --git a/conf/log4j2.xml.template b/conf/log4j2.xml.template index 2601690eb90..215fddf47f4 100644 --- a/conf/log4j2.xml.template +++ b/conf/log4j2.xml.template @@ -21,10 +21,9 @@ Set to debug or trace if log4j initialization is failing. --> + ${env:KYUUBI_LOG_DIR} rest-audit.log rest-audit-%d{yyyy-MM-dd}-%i.log - - k8s-audit.log k8s-audit-%d{yyyy-MM-dd}-%i.log @@ -35,7 +34,7 @@ - @@ -43,7 +42,7 @@ - diff --git a/dev/dependencyList b/dev/dependencyList index ede67c96173..4089d963d7a 100644 --- a/dev/dependencyList +++ b/dev/dependencyList @@ -140,28 +140,28 @@ log4j-core/2.20.0//log4j-core-2.20.0.jar log4j-slf4j-impl/2.20.0//log4j-slf4j-impl-2.20.0.jar logging-interceptor/3.12.12//logging-interceptor-3.12.12.jar lz4-java/1.8.0//lz4-java-1.8.0.jar -metrics-core/4.2.8//metrics-core-4.2.8.jar -metrics-jmx/4.2.8//metrics-jmx-4.2.8.jar -metrics-json/4.2.8//metrics-json-4.2.8.jar -metrics-jvm/4.2.8//metrics-jvm-4.2.8.jar +metrics-core/4.2.23//metrics-core-4.2.23.jar +metrics-jmx/4.2.23//metrics-jmx-4.2.23.jar +metrics-json/4.2.23//metrics-json-4.2.23.jar +metrics-jvm/4.2.23//metrics-jvm-4.2.23.jar mimepull/1.9.15//mimepull-1.9.15.jar -netty-all/4.1.93.Final//netty-all-4.1.93.Final.jar -netty-buffer/4.1.93.Final//netty-buffer-4.1.93.Final.jar -netty-codec-dns/4.1.93.Final//netty-codec-dns-4.1.93.Final.jar -netty-codec-http/4.1.93.Final//netty-codec-http-4.1.93.Final.jar -netty-codec-http2/4.1.93.Final//netty-codec-http2-4.1.93.Final.jar -netty-codec-socks/4.1.93.Final//netty-codec-socks-4.1.93.Final.jar -netty-codec/4.1.93.Final//netty-codec-4.1.93.Final.jar -netty-common/4.1.93.Final//netty-common-4.1.93.Final.jar -netty-handler-proxy/4.1.93.Final//netty-handler-proxy-4.1.93.Final.jar -netty-handler/4.1.93.Final//netty-handler-4.1.93.Final.jar -netty-resolver-dns/4.1.93.Final//netty-resolver-dns-4.1.93.Final.jar -netty-resolver/4.1.93.Final//netty-resolver-4.1.93.Final.jar -netty-transport-classes-epoll/4.1.93.Final//netty-transport-classes-epoll-4.1.93.Final.jar -netty-transport-native-epoll/4.1.93.Final/linux-aarch_64/netty-transport-native-epoll-4.1.93.Final-linux-aarch_64.jar -netty-transport-native-epoll/4.1.93.Final/linux-x86_64/netty-transport-native-epoll-4.1.93.Final-linux-x86_64.jar -netty-transport-native-unix-common/4.1.93.Final//netty-transport-native-unix-common-4.1.93.Final.jar -netty-transport/4.1.93.Final//netty-transport-4.1.93.Final.jar +netty-all/4.1.100.Final//netty-all-4.1.100.Final.jar +netty-buffer/4.1.100.Final//netty-buffer-4.1.100.Final.jar +netty-codec-dns/4.1.100.Final//netty-codec-dns-4.1.100.Final.jar +netty-codec-http/4.1.100.Final//netty-codec-http-4.1.100.Final.jar +netty-codec-http2/4.1.100.Final//netty-codec-http2-4.1.100.Final.jar +netty-codec-socks/4.1.100.Final//netty-codec-socks-4.1.100.Final.jar +netty-codec/4.1.100.Final//netty-codec-4.1.100.Final.jar +netty-common/4.1.100.Final//netty-common-4.1.100.Final.jar +netty-handler-proxy/4.1.100.Final//netty-handler-proxy-4.1.100.Final.jar +netty-handler/4.1.100.Final//netty-handler-4.1.100.Final.jar +netty-resolver-dns/4.1.100.Final//netty-resolver-dns-4.1.100.Final.jar +netty-resolver/4.1.100.Final//netty-resolver-4.1.100.Final.jar +netty-transport-classes-epoll/4.1.100.Final//netty-transport-classes-epoll-4.1.100.Final.jar +netty-transport-native-epoll/4.1.100.Final/linux-aarch_64/netty-transport-native-epoll-4.1.100.Final-linux-aarch_64.jar +netty-transport-native-epoll/4.1.100.Final/linux-x86_64/netty-transport-native-epoll-4.1.100.Final-linux-x86_64.jar +netty-transport-native-unix-common/4.1.100.Final//netty-transport-native-unix-common-4.1.100.Final.jar +netty-transport/4.1.100.Final//netty-transport-4.1.100.Final.jar okhttp-urlconnection/3.14.9//okhttp-urlconnection-3.14.9.jar okhttp/3.12.12//okhttp-3.12.12.jar okio/1.15.0//okio-1.15.0.jar diff --git a/dev/kyuubi-codecov/pom.xml b/dev/kyuubi-codecov/pom.xml index a5ec582f961..cdf79827359 100644 --- a/dev/kyuubi-codecov/pom.xml +++ b/dev/kyuubi-codecov/pom.xml @@ -31,6 +31,18 @@ https://kyuubi.apache.org/ + + org.apache.kyuubi + kyuubi-util + ${project.version} + + + + org.apache.kyuubi + kyuubi-util-scala_${scala.binary.version} + ${project.version} + + org.apache.kyuubi kyuubi-common_${scala.binary.version} @@ -130,26 +142,6 @@ - - org.jacoco - jacoco-maven-plugin - - - report-agg - - report-aggregate - - verify - - - **/jacoco*.exec - - ${project.reporting.outputDirectory}/jacoco-aggregate-all - - - - - org.apache.maven.plugins maven-dependency-plugin @@ -229,5 +221,31 @@ + + codecov + + + + org.jacoco + jacoco-maven-plugin + + + report-agg + + report-aggregate + + verify + + + **/jacoco*.exec + + ${project.reporting.outputDirectory}/jacoco-aggregate-all + + + + + + + diff --git a/docker/playground/.env b/docker/playground/.env index 24284bd39fa..e8446fd56c9 100644 --- a/docker/playground/.env +++ b/docker/playground/.env @@ -18,13 +18,13 @@ AWS_JAVA_SDK_VERSION=1.12.367 HADOOP_VERSION=3.3.6 HIVE_VERSION=2.3.9 -ICEBERG_VERSION=1.3.1 -KYUUBI_VERSION=1.7.3 -KYUUBI_HADOOP_VERSION=3.3.5 +ICEBERG_VERSION=1.4.2 +KYUUBI_VERSION=1.8.0 +KYUUBI_HADOOP_VERSION=3.3.6 POSTGRES_VERSION=12 POSTGRES_JDBC_VERSION=42.3.4 SCALA_BINARY_VERSION=2.12 -SPARK_VERSION=3.3.3 -SPARK_BINARY_VERSION=3.3 -SPARK_HADOOP_VERSION=3.3.2 +SPARK_VERSION=3.4.2 +SPARK_BINARY_VERSION=3.4 +SPARK_HADOOP_VERSION=3.3.4 ZOOKEEPER_VERSION=3.6.3 diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css deleted file mode 100644 index 9352af86567..00000000000 --- a/docs/_static/css/custom.css +++ /dev/null @@ -1,47 +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. - */ - -table.docutils { - width: 100%; - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; - table-layout: auto; -} -table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 1px; - border-left: 1px; - border-right: 1px; - border-bottom: 1px solid #aaa; -} -table.docutils td { - word-break: break-word; - min-width: 10%; -} -table.docutils tr:hover { - background: #efefef; -} -table.docutils tbody tr:nth-child(2n) { - background: #9EBCE21E; -} -table.docutils td:nth-child(1) { - width: 25%; - word-break: break-all; - font-weight: 500; -} diff --git a/docs/client/jdbc/kyuubi_jdbc.rst b/docs/client/jdbc/kyuubi_jdbc.rst index d4270ea8ac6..a3c56b41813 100644 --- a/docs/client/jdbc/kyuubi_jdbc.rst +++ b/docs/client/jdbc/kyuubi_jdbc.rst @@ -147,6 +147,28 @@ Connection URL over Service Discovery - zookeeper quorum is the corresponding zookeeper cluster configured by `kyuubi.ha.addresses` at the server side. - zooKeeperNamespace is the corresponding namespace configured by `kyuubi.ha.namespace` at the server side. +HiveServer2 Compatibility +************************* + +.. versionadded:: 1.8.0 + +JDBC Drivers need to negotiate a protocol version with Kyuubi Server/HiveServer2 when connecting. + +Kyuubi Hive JDBC Driver offers protocol version v10 (`clientProtocolVersion=9`, supported since Hive 2.3.0) +to server by default. + +If you need to connect to HiveServer2 before 2.3.0, +please set client property `clientProtocolVersion` to a lower number. + +.. code-block:: jdbc + + jdbc:subprotocol://host:port[/catalog]/[schema];clientProtocolVersion=9; + + +.. tip:: + All supported protocol versions and corresponding Hive versions can be found in `TProtocolVersion.java`_ + and its git commits. + Kerberos Authentication ----------------------- Since 1.6.0, Kyuubi JDBC driver implements the Kerberos authentication based on JAAS framework instead of `Hadoop UserGroupInformation`_, @@ -172,6 +194,7 @@ It's straightforward to use principal and keytab for Kerberos authentication, ju - kyuubiClientPrincipal: Kerberos ``principal`` for client authentication - kyuubiClientKeytab: path of Kerberos ``keytab`` file for client authentication +- kyuubiClientTicketCache: path of Kerberos ``ticketCache`` file for client authentication, available since 1.8.0. - kyuubiServerPrincipal: Kerberos ``principal`` configured by `kyuubi.kinit.principal` at the server side. ``kyuubiServerPrincipal`` is available as an alias of ``principal`` since 1.7.0, use ``principal`` for previous versions. @@ -218,4 +241,5 @@ Authentication by Subject (programing only) .. _JDBC Applications: ../bi_tools/index.html .. _java.sql.DriverManager: https://docs.oracle.com/javase/8/docs/api/java/sql/DriverManager.html .. _Hadoop UserGroupInformation: https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/security/UserGroupInformation.html -.. _krb5.conf instruction: https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/KerberosReq.html \ No newline at end of file +.. _krb5.conf instruction: https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/KerberosReq.html +.. _TProtocolVersion.java: https://github.com/apache/hive/blob/master/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProtocolVersion.java \ No newline at end of file diff --git a/docs/client/rest/rest_api.md b/docs/client/rest/rest_api.md index fc04857d020..4f28dec05ac 100644 --- a/docs/client/rest/rest_api.md +++ b/docs/client/rest/rest_api.md @@ -297,7 +297,7 @@ Get a list of operation log lines of the running operation by the specified oper | Name | Description | Type | |:--------|:--------------------------------------|:-----| -| maxRows | The max row that are pulled each time | Int | +| maxrows | The max row that are pulled each time | Int | #### Response Body @@ -410,12 +410,6 @@ The [Batch](#batch). Kill the batch if it is still running. -#### Request Parameters - -| Name | Description | Type | -|:------------------------|:------------------------------|:-----------------| -| hive.server2.proxy.user | the proxy user to impersonate | String(optional) | - #### Response Body | Name | Description | Type | @@ -468,8 +462,12 @@ Delete the specified engine. | type | the engine type | String(optional) | | sharelevel | the engine share level | String(optional) | | subdomain | the engine subdomain | String(optional) | +| proxyUser | the proxy user to impersonate | String(optional) | | hive.server2.proxy.user | the proxy user to impersonate | String(optional) | +`proxyUser` is an alternative to `hive.server2.proxy.user`, and the current behavior is consistent with +`hive.server2.proxy.user`. When both parameters are set, `proxyUser` takes precedence. + ### GET /admin/engine Get a list of satisfied engines. @@ -481,8 +479,12 @@ Get a list of satisfied engines. | type | the engine type | String(optional) | | sharelevel | the engine share level | String(optional) | | subdomain | the engine subdomain | String(optional) | +| proxyUser | the proxy user to impersonate | String(optional) | | hive.server2.proxy.user | the proxy user to impersonate | String(optional) | +`proxyUser` is an alternative to hive.server2.proxy.user, and the current behavior is consistent with +hive.server2.proxy.user. When both parameters are set, proxyUser takes precedence. + #### Response Body The [Engine](#engine) List. diff --git a/docs/client/ui/engine_ui.md b/docs/client/ui/engine_ui.md new file mode 100644 index 00000000000..312606eca5e --- /dev/null +++ b/docs/client/ui/engine_ui.md @@ -0,0 +1,39 @@ + + +# Engine UI + +This engine UI is able to help you understand status of the engine behind Kyuubi servers. + +## Engine Management Details + +The Engine UI offers an Engine Management feature on the left side of UI page. This allows users to access detailed information about the engines. +However, not all available engines are displayed by default. Thus, users have to add correct filter conditions to get engines they prefer. After setting the right conditions, please click on 'search' button. +The engines that meet your specified requirements should be listed on the page as the below picture shown. + +![workspace](../../imgs/ui/engine_ui.png) + +| Name | Description | +|:---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Engine address | The engine IP address | +| Engine ID | The unique identifier of engine | +| Engine Type | The engine types(only SPARK-SQL engine can be shown in this page now) | +| Share Level | The share level of engine, such as user, connection, group and server | +| User | The user created the engine | +| Version | The version of the Kyuubi server associated with this engine | +| Operation | Extra operations that users can do further.
1. View native engine UI
find and select the engine you wish to view its native UI.
clink on the view button, you should be redirected to the native engine UI powered by Kyuubi proxy.
2. Delete the specified engine gracefully
select the specific engine you would like to delete from the Engine Management page.
click on delete button and confirm your choice, then the engine will be remove from service discovery like Zookeeper, ETCD and etc.
The engine will eventually be shut down once all connected session closed. | + diff --git a/docs/client/ui/index.rst b/docs/client/ui/index.rst index 63a02cbd484..7ac3b9d2881 100644 --- a/docs/client/ui/index.rst +++ b/docs/client/ui/index.rst @@ -20,5 +20,5 @@ Web UI .. toctree:: :maxdepth: 2 - hive_beeline + engine_ui diff --git a/docs/conf.py b/docs/conf.py index eaac1acedef..d75f819b3c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -126,8 +126,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_css_files = ["css/custom.css"] +html_static_path = [] htmlhelp_basename = 'Recommonmarkdoc' github_doc_root = 'https://github.com/apache/kyuubi/tree/master/docs/' diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index 66fa4557893..20a1bf8d93f 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -120,82 +120,91 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co ### Engine -| Key | Default | Meaning | Type | Since | -|----------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| -| kyuubi.engine.chat.extra.classpath | <undefined> | The extra classpath for the Chat engine, for configuring the location of the SDK and etc. | string | 1.8.0 | -| kyuubi.engine.chat.gpt.apiKey | <undefined> | The key to access OpenAI open API, which could be got at https://platform.openai.com/account/api-keys | string | 1.8.0 | -| kyuubi.engine.chat.gpt.http.connect.timeout | PT2M | The timeout[ms] for establishing the connection with the Chat GPT server. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.8.0 | -| kyuubi.engine.chat.gpt.http.proxy | <undefined> | HTTP proxy url for API calling in Chat GPT engine. e.g. http://127.0.0.1:1087 | string | 1.8.0 | -| kyuubi.engine.chat.gpt.http.socket.timeout | PT2M | The timeout[ms] for waiting for data packets after Chat GPT server connection is established. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.8.0 | -| kyuubi.engine.chat.gpt.model | gpt-3.5-turbo | ID of the model used in ChatGPT. Available models refer to OpenAI's [Model overview](https://platform.openai.com/docs/models/overview). | string | 1.8.0 | -| kyuubi.engine.chat.java.options | <undefined> | The extra Java options for the Chat engine | string | 1.8.0 | -| kyuubi.engine.chat.memory | 1g | The heap memory for the Chat engine | string | 1.8.0 | -| kyuubi.engine.chat.provider | ECHO | The provider for the Chat engine. Candidates:
  • ECHO: simply replies a welcome message.
  • GPT: a.k.a ChatGPT, powered by OpenAI.
| string | 1.8.0 | -| kyuubi.engine.connection.url.use.hostname | true | (deprecated) When true, the engine registers with hostname to zookeeper. When Spark runs on K8s with cluster mode, set to false to ensure that server can connect to engine | boolean | 1.3.0 | -| kyuubi.engine.deregister.exception.classes || A comma-separated list of exception classes. If there is any exception thrown, whose class matches the specified classes, the engine would deregister itself. | set | 1.2.0 | -| kyuubi.engine.deregister.exception.messages || A comma-separated list of exception messages. If there is any exception thrown, whose message or stacktrace matches the specified message list, the engine would deregister itself. | set | 1.2.0 | -| kyuubi.engine.deregister.exception.ttl | PT30M | Time to live(TTL) for exceptions pattern specified in kyuubi.engine.deregister.exception.classes and kyuubi.engine.deregister.exception.messages to deregister engines. Once the total error count hits the kyuubi.engine.deregister.job.max.failures within the TTL, an engine will deregister itself and wait for self-terminated. Otherwise, we suppose that the engine has recovered from temporary failures. | duration | 1.2.0 | -| kyuubi.engine.deregister.job.max.failures | 4 | Number of failures of job before deregistering the engine. | int | 1.2.0 | -| kyuubi.engine.event.json.log.path | file:///tmp/kyuubi/events | The location where all the engine events go for the built-in JSON logger.
  • Local Path: start with 'file://'
  • HDFS Path: start with 'hdfs://'
| string | 1.3.0 | -| kyuubi.engine.event.loggers | SPARK | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
  • SPARK: the events will be written to the Spark listener bus.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: User-defined event handlers.
Note that: Kyuubi supports custom event handlers with the Java SPI. To register a custom event handler, the user needs to implement a subclass of `org.apache.kyuubi.events.handler.CustomEventHandlerProvider` which has a zero-arg constructor. | seq | 1.3.0 | -| kyuubi.engine.flink.application.jars | <undefined> | A comma-separated list of the local jars to be shipped with the job to the cluster. For example, SQL UDF jars. Only effective in yarn application mode. | string | 1.8.0 | -| kyuubi.engine.flink.extra.classpath | <undefined> | The extra classpath for the Flink SQL engine, for configuring the location of hadoop client jars, etc. Only effective in yarn session mode. | string | 1.6.0 | -| kyuubi.engine.flink.java.options | <undefined> | The extra Java options for the Flink SQL engine. Only effective in yarn session mode. | string | 1.6.0 | -| kyuubi.engine.flink.memory | 1g | The heap memory for the Flink SQL engine. Only effective in yarn session mode. | string | 1.6.0 | -| kyuubi.engine.hive.event.loggers | JSON | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: to be done.
| seq | 1.7.0 | -| kyuubi.engine.hive.extra.classpath | <undefined> | The extra classpath for the Hive query engine, for configuring location of the hadoop client jars and etc. | string | 1.6.0 | -| kyuubi.engine.hive.java.options | <undefined> | The extra Java options for the Hive query engine | string | 1.6.0 | -| kyuubi.engine.hive.memory | 1g | The heap memory for the Hive query engine | string | 1.6.0 | -| kyuubi.engine.initialize.sql | SHOW DATABASES | SemiColon-separated list of SQL statements to be initialized in the newly created engine before queries. i.e. use `SHOW DATABASES` to eagerly active HiveClient. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver. | seq | 1.2.0 | -| kyuubi.engine.jdbc.connection.password | <undefined> | The password is used for connecting to server | string | 1.6.0 | -| kyuubi.engine.jdbc.connection.propagateCredential | false | Whether to use the session's user and password to connect to database | boolean | 1.8.0 | -| kyuubi.engine.jdbc.connection.properties || The additional properties are used for connecting to server | seq | 1.6.0 | -| kyuubi.engine.jdbc.connection.provider | <undefined> | The connection provider is used for getting a connection from the server | string | 1.6.0 | -| kyuubi.engine.jdbc.connection.url | <undefined> | The server url that engine will connect to | string | 1.6.0 | -| kyuubi.engine.jdbc.connection.user | <undefined> | The user is used for connecting to server | string | 1.6.0 | -| kyuubi.engine.jdbc.driver.class | <undefined> | The driver class for JDBC engine connection | string | 1.6.0 | -| kyuubi.engine.jdbc.extra.classpath | <undefined> | The extra classpath for the JDBC query engine, for configuring the location of the JDBC driver and etc. | string | 1.6.0 | -| kyuubi.engine.jdbc.initialize.sql | SELECT 1 | SemiColon-separated list of SQL statements to be initialized in the newly created engine before queries. i.e. use `SELECT 1` to eagerly active JDBCClient. | seq | 1.8.0 | -| kyuubi.engine.jdbc.java.options | <undefined> | The extra Java options for the JDBC query engine | string | 1.6.0 | -| kyuubi.engine.jdbc.memory | 1g | The heap memory for the JDBC query engine | string | 1.6.0 | -| kyuubi.engine.jdbc.session.initialize.sql || SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. | seq | 1.8.0 | -| kyuubi.engine.jdbc.type | <undefined> | The short name of JDBC type | string | 1.6.0 | -| kyuubi.engine.kubernetes.submit.timeout | PT30S | The engine submit timeout for Kubernetes application. | duration | 1.7.2 | -| kyuubi.engine.operation.convert.catalog.database.enabled | true | When set to true, The engine converts the JDBC methods of set/get Catalog and set/get Schema to the implementation of different engines | boolean | 1.6.0 | -| kyuubi.engine.operation.log.dir.root | engine_operation_logs | Root directory for query operation log at engine-side. | string | 1.4.0 | -| kyuubi.engine.pool.name | engine-pool | The name of the engine pool. | string | 1.5.0 | -| kyuubi.engine.pool.selectPolicy | RANDOM | The select policy of an engine from the corresponding engine pool engine for a session.
  • RANDOM - Randomly use the engine in the pool
  • POLLING - Polling use the engine in the pool
| string | 1.7.0 | -| kyuubi.engine.pool.size | -1 | The size of the engine pool. Note that, if the size is less than 1, the engine pool will not be enabled; otherwise, the size of the engine pool will be min(this, kyuubi.engine.pool.size.threshold). | int | 1.4.0 | -| kyuubi.engine.pool.size.threshold | 9 | This parameter is introduced as a server-side parameter controlling the upper limit of the engine pool. | int | 1.4.0 | -| kyuubi.engine.session.initialize.sql || SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver. | seq | 1.3.0 | -| kyuubi.engine.share.level | USER | Engines will be shared in different levels, available configs are:
  • CONNECTION: engine will not be shared but only used by the current client connection
  • USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain
  • GROUP: the engine will be shared by all sessions created by all users belong to the same primary group name. The engine will be launched by the group name as the effective username, so here the group name is in value of special user who is able to visit the computing resources/data of the team. It follows the [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the primary group is not found, it fallback to the USER level.
  • SERVER: the App will be shared by Kyuubi servers
| string | 1.2.0 | -| kyuubi.engine.share.level.sub.domain | <undefined> | (deprecated) - Using kyuubi.engine.share.level.subdomain instead | string | 1.2.0 | -| kyuubi.engine.share.level.subdomain | <undefined> | Allow end-users to create a subdomain for the share level of an engine. A subdomain is a case-insensitive string values that must be a valid zookeeper subpath. For example, for the `USER` share level, an end-user can share a certain engine within a subdomain, not for all of its clients. End-users are free to create multiple engines in the `USER` share level. When disable engine pool, use 'default' if absent. | string | 1.4.0 | -| kyuubi.engine.single.spark.session | false | When set to true, this engine is running in a single session mode. All the JDBC/ODBC connections share the temporary views, function registries, SQL configuration and the current database. | boolean | 1.3.0 | -| kyuubi.engine.spark.event.loggers | SPARK | A comma-separated list of engine loggers, where engine/session/operation etc events go.
  • SPARK: the events will be written to the Spark listener bus.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: to be done.
| seq | 1.7.0 | -| kyuubi.engine.spark.python.env.archive | <undefined> | Portable Python env archive used for Spark engine Python language mode. | string | 1.7.0 | -| kyuubi.engine.spark.python.env.archive.exec.path | bin/python | The Python exec path under the Python env archive. | string | 1.7.0 | -| kyuubi.engine.spark.python.home.archive | <undefined> | Spark archive containing $SPARK_HOME/python directory, which is used to init session Python worker for Python language mode. | string | 1.7.0 | -| kyuubi.engine.submit.timeout | PT30S | Period to tolerant Driver Pod ephemerally invisible after submitting. In some Resource Managers, e.g. K8s, the Driver Pod is not visible immediately after `spark-submit` is returned. | duration | 1.7.1 | -| kyuubi.engine.trino.connection.keystore.password | <undefined> | The keystore password used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.keystore.path | <undefined> | The keystore path used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.keystore.type | <undefined> | The keystore type used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.password | <undefined> | The password used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.truststore.password | <undefined> | The truststore password used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.truststore.path | <undefined> | The truststore path used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.connection.truststore.type | <undefined> | The truststore type used for connecting to trino cluster | string | 1.8.0 | -| kyuubi.engine.trino.event.loggers | JSON | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: to be done.
| seq | 1.7.0 | -| kyuubi.engine.trino.extra.classpath | <undefined> | The extra classpath for the Trino query engine, for configuring other libs which may need by the Trino engine | string | 1.6.0 | -| kyuubi.engine.trino.java.options | <undefined> | The extra Java options for the Trino query engine | string | 1.6.0 | -| kyuubi.engine.trino.memory | 1g | The heap memory for the Trino query engine | string | 1.6.0 | -| kyuubi.engine.type | SPARK_SQL | Specify the detailed engine supported by Kyuubi. The engine type bindings to SESSION scope. This configuration is experimental. Currently, available configs are:
  • SPARK_SQL: specify this engine type will launch a Spark engine which can provide all the capacity of the Apache Spark. Note, it's a default engine type.
  • FLINK_SQL: specify this engine type will launch a Flink engine which can provide all the capacity of the Apache Flink.
  • TRINO: specify this engine type will launch a Trino engine which can provide all the capacity of the Trino.
  • HIVE_SQL: specify this engine type will launch a Hive engine which can provide all the capacity of the Hive Server2.
  • JDBC: specify this engine type will launch a JDBC engine which can forward queries to the database system through the certain JDBC driver, for now, it supports Doris and Phoenix.
  • CHAT: specify this engine type will launch a Chat engine.
| string | 1.4.0 | -| kyuubi.engine.ui.retainedSessions | 200 | The number of SQL client sessions kept in the Kyuubi Query Engine web UI. | int | 1.4.0 | -| kyuubi.engine.ui.retainedStatements | 200 | The number of statements kept in the Kyuubi Query Engine web UI. | int | 1.4.0 | -| kyuubi.engine.ui.stop.enabled | true | When true, allows Kyuubi engine to be killed from the Spark Web UI. | boolean | 1.3.0 | -| kyuubi.engine.user.isolated.spark.session | true | When set to false, if the engine is running in a group or server share level, all the JDBC/ODBC connections will be isolated against the user. Including the temporary views, function registries, SQL configuration, and the current database. Note that, it does not affect if the share level is connection or user. | boolean | 1.6.0 | -| kyuubi.engine.user.isolated.spark.session.idle.interval | PT1M | The interval to check if the user-isolated Spark session is timeout. | duration | 1.6.0 | -| kyuubi.engine.user.isolated.spark.session.idle.timeout | PT6H | If kyuubi.engine.user.isolated.spark.session is false, we will release the Spark session if its corresponding user is inactive after this configured timeout. | duration | 1.6.0 | -| kyuubi.engine.yarn.submit.timeout | PT30S | The engine submit timeout for YARN application. | duration | 1.7.2 | +| Key | Default | Meaning | Type | Since | +|----------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| +| kyuubi.engine.chat.ernie.http.connect.timeout | PT2M | The timeout[ms] for establishing the connection with the ernie bot server. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.9.0 | +| kyuubi.engine.chat.ernie.http.proxy | <undefined> | HTTP proxy url for API calling in ernie bot engine. e.g. http://127.0.0.1:1088 | string | 1.9.0 | +| kyuubi.engine.chat.ernie.http.socket.timeout | PT2M | The timeout[ms] for waiting for data packets after ernie bot server connection is established. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.9.0 | +| kyuubi.engine.chat.ernie.model | completions | ID of the model used in ernie bot. Available models are completions_pro, ernie_bot_8k, completions and eb-instant[Model overview](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/6lp69is2a). | string | 1.9.0 | +| kyuubi.engine.chat.ernie.token | <undefined> | The token to access ernie bot open API, which could be got at https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5 | string | 1.9.0 | +| kyuubi.engine.chat.extra.classpath | <undefined> | The extra classpath for the Chat engine, for configuring the location of the SDK and etc. | string | 1.8.0 | +| kyuubi.engine.chat.gpt.apiKey | <undefined> | The key to access OpenAI open API, which could be got at https://platform.openai.com/account/api-keys | string | 1.8.0 | +| kyuubi.engine.chat.gpt.http.connect.timeout | PT2M | The timeout[ms] for establishing the connection with the Chat GPT server. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.8.0 | +| kyuubi.engine.chat.gpt.http.proxy | <undefined> | HTTP proxy url for API calling in Chat GPT engine. e.g. http://127.0.0.1:1087 | string | 1.8.0 | +| kyuubi.engine.chat.gpt.http.socket.timeout | PT2M | The timeout[ms] for waiting for data packets after Chat GPT server connection is established. A timeout value of zero is interpreted as an infinite timeout. | duration | 1.8.0 | +| kyuubi.engine.chat.gpt.model | gpt-3.5-turbo | ID of the model used in ChatGPT. Available models refer to OpenAI's [Model overview](https://platform.openai.com/docs/models/overview). | string | 1.8.0 | +| kyuubi.engine.chat.java.options | <undefined> | The extra Java options for the Chat engine | string | 1.8.0 | +| kyuubi.engine.chat.memory | 1g | The heap memory for the Chat engine | string | 1.8.0 | +| kyuubi.engine.chat.provider | ECHO | The provider for the Chat engine. Candidates:
  • ECHO: simply replies a welcome message.
  • GPT: a.k.a ChatGPT, powered by OpenAI.
  • ERNIE: ErnieBot, powered by Baidu.
| string | 1.8.0 | +| kyuubi.engine.connection.url.use.hostname | true | (deprecated) When true, the engine registers with hostname to zookeeper. When Spark runs on K8s with cluster mode, set to false to ensure that server can connect to engine | boolean | 1.3.0 | +| kyuubi.engine.deregister.exception.classes || A comma-separated list of exception classes. If there is any exception thrown, whose class matches the specified classes, the engine would deregister itself. | set | 1.2.0 | +| kyuubi.engine.deregister.exception.messages || A comma-separated list of exception messages. If there is any exception thrown, whose message or stacktrace matches the specified message list, the engine would deregister itself. | set | 1.2.0 | +| kyuubi.engine.deregister.exception.ttl | PT30M | Time to live(TTL) for exceptions pattern specified in kyuubi.engine.deregister.exception.classes and kyuubi.engine.deregister.exception.messages to deregister engines. Once the total error count hits the kyuubi.engine.deregister.job.max.failures within the TTL, an engine will deregister itself and wait for self-terminated. Otherwise, we suppose that the engine has recovered from temporary failures. | duration | 1.2.0 | +| kyuubi.engine.deregister.job.max.failures | 4 | Number of failures of job before deregistering the engine. | int | 1.2.0 | +| kyuubi.engine.event.json.log.path | file:///tmp/kyuubi/events | The location where all the engine events go for the built-in JSON logger.
  • Local Path: start with 'file://'
  • HDFS Path: start with 'hdfs://'
| string | 1.3.0 | +| kyuubi.engine.event.loggers | SPARK | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
  • SPARK: the events will be written to the Spark listener bus.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: User-defined event handlers.
Note that: Kyuubi supports custom event handlers with the Java SPI. To register a custom event handler, the user needs to implement a subclass of `org.apache.kyuubi.events.handler.CustomEventHandlerProvider` which has a zero-arg constructor. | seq | 1.3.0 | +| kyuubi.engine.flink.application.jars | <undefined> | A comma-separated list of the local jars to be shipped with the job to the cluster. For example, SQL UDF jars. Only effective in yarn application mode. | string | 1.8.0 | +| kyuubi.engine.flink.extra.classpath | <undefined> | The extra classpath for the Flink SQL engine, for configuring the location of hadoop client jars, etc. Only effective in yarn session mode. | string | 1.6.0 | +| kyuubi.engine.flink.initialize.sql | SHOW DATABASES | The initialize sql for Flink engine. It fallback to `kyuubi.engine.initialize.sql`. | seq | 1.8.1 | +| kyuubi.engine.flink.java.options | <undefined> | The extra Java options for the Flink SQL engine. Only effective in yarn session mode. | string | 1.6.0 | +| kyuubi.engine.flink.memory | 1g | The heap memory for the Flink SQL engine. Only effective in yarn session mode. | string | 1.6.0 | +| kyuubi.engine.hive.event.loggers | JSON | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
  • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
  • JDBC: to be done
  • CUSTOM: to be done.
| seq | 1.7.0 | +| kyuubi.engine.hive.extra.classpath | <undefined> | The extra classpath for the Hive query engine, for configuring location of the hadoop client jars and etc. | string | 1.6.0 | +| kyuubi.engine.hive.java.options | <undefined> | The extra Java options for the Hive query engine | string | 1.6.0 | +| kyuubi.engine.hive.memory | 1g | The heap memory for the Hive query engine | string | 1.6.0 | +| kyuubi.engine.initialize.sql | SHOW DATABASES | SemiColon-separated list of SQL statements to be initialized in the newly created engine before queries. i.e. use `SHOW DATABASES` to eagerly active HiveClient. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver. | seq | 1.2.0 | +| kyuubi.engine.jdbc.connection.password | <undefined> | The password is used for connecting to server | string | 1.6.0 | +| kyuubi.engine.jdbc.connection.propagateCredential | false | Whether to use the session's user and password to connect to database | boolean | 1.8.0 | +| kyuubi.engine.jdbc.connection.properties || The additional properties are used for connecting to server | seq | 1.6.0 | +| kyuubi.engine.jdbc.connection.provider | <undefined> | A JDBC connection provider plugin for the Kyuubi Server to establish a connection to the JDBC URL. The configuration value should be a subclass of `org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider`. Kyuubi provides the following built-in implementations:
  • doris: For establishing Doris connections.
  • mysql: For establishing MySQL connections.
  • phoenix: For establishing Phoenix connections.
  • postgresql: For establishing PostgreSQL connections.
  • starrocks: For establishing StarRocks connections.
  • | string | 1.6.0 | +| kyuubi.engine.jdbc.connection.url | <undefined> | The server url that engine will connect to | string | 1.6.0 | +| kyuubi.engine.jdbc.connection.user | <undefined> | The user is used for connecting to server | string | 1.6.0 | +| kyuubi.engine.jdbc.driver.class | <undefined> | The driver class for JDBC engine connection | string | 1.6.0 | +| kyuubi.engine.jdbc.extra.classpath | <undefined> | The extra classpath for the JDBC query engine, for configuring the location of the JDBC driver and etc. | string | 1.6.0 | +| kyuubi.engine.jdbc.fetch.size | 1000 | The fetch size of JDBC engine | int | 1.9.0 | +| kyuubi.engine.jdbc.initialize.sql | SELECT 1 | SemiColon-separated list of SQL statements to be initialized in the newly created engine before queries. i.e. use `SELECT 1` to eagerly active JDBCClient. | seq | 1.8.0 | +| kyuubi.engine.jdbc.java.options | <undefined> | The extra Java options for the JDBC query engine | string | 1.6.0 | +| kyuubi.engine.jdbc.memory | 1g | The heap memory for the JDBC query engine | string | 1.6.0 | +| kyuubi.engine.jdbc.session.initialize.sql || SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. | seq | 1.8.0 | +| kyuubi.engine.jdbc.type | <undefined> | The short name of JDBC type | string | 1.6.0 | +| kyuubi.engine.kubernetes.submit.timeout | PT30S | The engine submit timeout for Kubernetes application. | duration | 1.7.2 | +| kyuubi.engine.operation.convert.catalog.database.enabled | true | When set to true, The engine converts the JDBC methods of set/get Catalog and set/get Schema to the implementation of different engines | boolean | 1.6.0 | +| kyuubi.engine.operation.log.dir.root | engine_operation_logs | Root directory for query operation log at engine-side. | string | 1.4.0 | +| kyuubi.engine.pool.name | engine-pool | The name of the engine pool. | string | 1.5.0 | +| kyuubi.engine.pool.selectPolicy | RANDOM | The select policy of an engine from the corresponding engine pool engine for a session.
    • RANDOM - Randomly use the engine in the pool
    • POLLING - Polling use the engine in the pool
    | string | 1.7.0 | +| kyuubi.engine.pool.size | -1 | The size of the engine pool. Note that, if the size is less than 1, the engine pool will not be enabled; otherwise, the size of the engine pool will be min(this, kyuubi.engine.pool.size.threshold). | int | 1.4.0 | +| kyuubi.engine.pool.size.threshold | 9 | This parameter is introduced as a server-side parameter controlling the upper limit of the engine pool. | int | 1.4.0 | +| kyuubi.engine.session.initialize.sql || SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver. | seq | 1.3.0 | +| kyuubi.engine.share.level | USER | Engines will be shared in different levels, available configs are:
    • CONNECTION: engine will not be shared but only used by the current client connection
    • USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain
    • GROUP: the engine will be shared by all sessions created by all users belong to the same primary group name. The engine will be launched by the group name as the effective username, so here the group name is in value of special user who is able to visit the computing resources/data of the team. It follows the [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the primary group is not found, it fallback to the USER level.
    • SERVER: the App will be shared by Kyuubi servers
    | string | 1.2.0 | +| kyuubi.engine.share.level.sub.domain | <undefined> | (deprecated) - Using kyuubi.engine.share.level.subdomain instead | string | 1.2.0 | +| kyuubi.engine.share.level.subdomain | <undefined> | Allow end-users to create a subdomain for the share level of an engine. A subdomain is a case-insensitive string values that must be a valid zookeeper subpath. For example, for the `USER` share level, an end-user can share a certain engine within a subdomain, not for all of its clients. End-users are free to create multiple engines in the `USER` share level. When disable engine pool, use 'default' if absent. | string | 1.4.0 | +| kyuubi.engine.single.spark.session | false | When set to true, this engine is running in a single session mode. All the JDBC/ODBC connections share the temporary views, function registries, SQL configuration and the current database. | boolean | 1.3.0 | +| kyuubi.engine.spark.event.loggers | SPARK | A comma-separated list of engine loggers, where engine/session/operation etc events go.
    • SPARK: the events will be written to the Spark listener bus.
    • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
    • JDBC: to be done
    • CUSTOM: to be done.
    | seq | 1.7.0 | +| kyuubi.engine.spark.initialize.sql | SHOW DATABASES | The initialize sql for Spark engine. It fallback to `kyuubi.engine.initialize.sql`. | seq | 1.8.1 | +| kyuubi.engine.spark.output.mode | AUTO | The output mode of Spark engine:
    • AUTO: For PySpark, the extracted `text/plain` from python response as output.
    • NOTEBOOK: For PySpark, the original python response as output.
    | string | 1.9.0 | +| kyuubi.engine.spark.python.env.archive | <undefined> | Portable Python env archive used for Spark engine Python language mode. | string | 1.7.0 | +| kyuubi.engine.spark.python.env.archive.exec.path | bin/python | The Python exec path under the Python env archive. | string | 1.7.0 | +| kyuubi.engine.spark.python.home.archive | <undefined> | Spark archive containing $SPARK_HOME/python directory, which is used to init session Python worker for Python language mode. | string | 1.7.0 | +| kyuubi.engine.submit.timeout | PT30S | Period to tolerant Driver Pod ephemerally invisible after submitting. In some Resource Managers, e.g. K8s, the Driver Pod is not visible immediately after `spark-submit` is returned. | duration | 1.7.1 | +| kyuubi.engine.trino.connection.keystore.password | <undefined> | The keystore password used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.keystore.path | <undefined> | The keystore path used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.keystore.type | <undefined> | The keystore type used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.password | <undefined> | The password used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.truststore.password | <undefined> | The truststore password used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.truststore.path | <undefined> | The truststore path used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.connection.truststore.type | <undefined> | The truststore type used for connecting to trino cluster | string | 1.8.0 | +| kyuubi.engine.trino.event.loggers | JSON | A comma-separated list of engine history loggers, where engine/session/operation etc events go.
    • JSON: the events will be written to the location of kyuubi.engine.event.json.log.path
    • JDBC: to be done
    • CUSTOM: to be done.
    | seq | 1.7.0 | +| kyuubi.engine.trino.extra.classpath | <undefined> | The extra classpath for the Trino query engine, for configuring other libs which may need by the Trino engine | string | 1.6.0 | +| kyuubi.engine.trino.java.options | <undefined> | The extra Java options for the Trino query engine | string | 1.6.0 | +| kyuubi.engine.trino.memory | 1g | The heap memory for the Trino query engine | string | 1.6.0 | +| kyuubi.engine.type | SPARK_SQL | Specify the detailed engine supported by Kyuubi. The engine type bindings to SESSION scope. This configuration is experimental. Currently, available configs are:
    • SPARK_SQL: specify this engine type will launch a Spark engine which can provide all the capacity of the Apache Spark. Note, it's a default engine type.
    • FLINK_SQL: specify this engine type will launch a Flink engine which can provide all the capacity of the Apache Flink.
    • TRINO: specify this engine type will launch a Trino engine which can provide all the capacity of the Trino.
    • HIVE_SQL: specify this engine type will launch a Hive engine which can provide all the capacity of the Hive Server2.
    • JDBC: specify this engine type will launch a JDBC engine which can forward queries to the database system through the certain JDBC driver, for now, it supports Doris, MySQL, Phoenix, PostgreSQL and StarRocks.
    • CHAT: specify this engine type will launch a Chat engine.
    | string | 1.4.0 | +| kyuubi.engine.ui.retainedSessions | 200 | The number of SQL client sessions kept in the Kyuubi Query Engine web UI. | int | 1.4.0 | +| kyuubi.engine.ui.retainedStatements | 200 | The number of statements kept in the Kyuubi Query Engine web UI. | int | 1.4.0 | +| kyuubi.engine.ui.stop.enabled | true | When true, allows Kyuubi engine to be killed from the Spark Web UI. | boolean | 1.3.0 | +| kyuubi.engine.user.isolated.spark.session | true | When set to false, if the engine is running in a group or server share level, all the JDBC/ODBC connections will be isolated against the user. Including the temporary views, function registries, SQL configuration, and the current database. Note that, it does not affect if the share level is connection or user. | boolean | 1.6.0 | +| kyuubi.engine.user.isolated.spark.session.idle.interval | PT1M | The interval to check if the user-isolated Spark session is timeout. | duration | 1.6.0 | +| kyuubi.engine.user.isolated.spark.session.idle.timeout | PT6H | If kyuubi.engine.user.isolated.spark.session is false, we will release the Spark session if its corresponding user is inactive after this configured timeout. | duration | 1.6.0 | +| kyuubi.engine.yarn.submit.timeout | PT30S | The engine submit timeout for YARN application. | duration | 1.7.2 | ### Event @@ -309,20 +318,26 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co ### Kubernetes -| Key | Default | Meaning | Type | Since | -|-----------------------------------------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| -| kyuubi.kubernetes.authenticate.caCertFile | <undefined> | Path to the CA cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | -| kyuubi.kubernetes.authenticate.clientCertFile | <undefined> | Path to the client cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | -| kyuubi.kubernetes.authenticate.clientKeyFile | <undefined> | Path to the client key file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | -| kyuubi.kubernetes.authenticate.oauthToken | <undefined> | The OAuth token to use when authenticating against the Kubernetes API server. Note that unlike, the other authentication options, this must be the exact string value of the token to use for the authentication. | string | 1.7.0 | -| kyuubi.kubernetes.authenticate.oauthTokenFile | <undefined> | Path to the file containing the OAuth token to use when authenticating against the Kubernetes API server. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | -| kyuubi.kubernetes.context | <undefined> | The desired context from your kubernetes config file used to configure the K8s client for interacting with the cluster. | string | 1.6.0 | -| kyuubi.kubernetes.context.allow.list || The allowed kubernetes context list, if it is empty, there is no kubernetes context limitation. | set | 1.8.0 | -| kyuubi.kubernetes.master.address | <undefined> | The internal Kubernetes master (API server) address to be used for kyuubi. | string | 1.7.0 | -| kyuubi.kubernetes.namespace | default | The namespace that will be used for running the kyuubi pods and find engines. | string | 1.7.0 | -| kyuubi.kubernetes.namespace.allow.list || The allowed kubernetes namespace list, if it is empty, there is no kubernetes namespace limitation. | set | 1.8.0 | -| kyuubi.kubernetes.terminatedApplicationRetainPeriod | PT5M | The period for which the Kyuubi server retains application information after the application terminates. | duration | 1.7.1 | -| kyuubi.kubernetes.trust.certificates | false | If set to true then client can submit to kubernetes cluster only with token | boolean | 1.7.0 | +| Key | Default | Meaning | Type | Since | +|----------------------------------------------------------------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| +| kyuubi.kubernetes.application.state.container | spark-kubernetes-driver | The container name to retrieve the application state from. | string | 1.8.1 | +| kyuubi.kubernetes.application.state.source | POD | The source to retrieve the application state from. The valid values are pod and container. If the source is container and there is container inside the pod with the name of kyuubi.kubernetes.application.state.container, the application state will be from the matched container state. Otherwise, the application state will be from the pod state. | string | 1.8.1 | +| kyuubi.kubernetes.authenticate.caCertFile | <undefined> | Path to the CA cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | +| kyuubi.kubernetes.authenticate.clientCertFile | <undefined> | Path to the client cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | +| kyuubi.kubernetes.authenticate.clientKeyFile | <undefined> | Path to the client key file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | +| kyuubi.kubernetes.authenticate.oauthToken | <undefined> | The OAuth token to use when authenticating against the Kubernetes API server. Note that unlike, the other authentication options, this must be the exact string value of the token to use for the authentication. | string | 1.7.0 | +| kyuubi.kubernetes.authenticate.oauthTokenFile | <undefined> | Path to the file containing the OAuth token to use when authenticating against the Kubernetes API server. Specify this as a path as opposed to a URI (i.e. do not provide a scheme) | string | 1.7.0 | +| kyuubi.kubernetes.context | <undefined> | The desired context from your kubernetes config file used to configure the K8s client for interacting with the cluster. | string | 1.6.0 | +| kyuubi.kubernetes.context.allow.list || The allowed kubernetes context list, if it is empty, there is no kubernetes context limitation. | set | 1.8.0 | +| kyuubi.kubernetes.master.address | <undefined> | The internal Kubernetes master (API server) address to be used for kyuubi. | string | 1.7.0 | +| kyuubi.kubernetes.namespace | default | The namespace that will be used for running the kyuubi pods and find engines. | string | 1.7.0 | +| kyuubi.kubernetes.namespace.allow.list || The allowed kubernetes namespace list, if it is empty, there is no kubernetes namespace limitation. | set | 1.8.0 | +| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.checkInterval | PT1M | Kyuubi server use guava cache as the cleanup trigger with time-based eviction, but the eviction would not happened until any get/put operation happened. This option schedule a daemon thread evict cache periodically. | duration | 1.8.1 | +| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.kind | NONE | Kyuubi server will delete the spark driver pod after the application terminates for kyuubi.kubernetes.terminatedApplicationRetainPeriod. Available options are NONE, ALL, COMPLETED and default value is None which means none of the pod will be deleted | string | 1.8.1 | +| kyuubi.kubernetes.spark.forciblyRewriteDriverPodName.enabled | false | Whether to forcibly rewrite Spark driver pod name with 'kyuubi--driver'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter pod name policies, thus the generated name may become illegal. | boolean | 1.8.1 | +| kyuubi.kubernetes.spark.forciblyRewriteExecutorPodNamePrefix.enabled | false | Whether to forcibly rewrite Spark executor pod name prefix with 'kyuubi-'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter Pod name policies, thus the generated name may become illegal. | boolean | 1.8.1 | +| kyuubi.kubernetes.terminatedApplicationRetainPeriod | PT5M | The period for which the Kyuubi server retains application information after the application terminates. | duration | 1.7.1 | +| kyuubi.kubernetes.trust.certificates | false | If set to true then client can submit to kubernetes cluster only with token | boolean | 1.7.0 | ### Lineage @@ -348,7 +363,7 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.metadata.store.jdbc.driver | <undefined> | JDBC driver class name for server jdbc metadata store. | string | 1.6.0 | | kyuubi.metadata.store.jdbc.password || The password for server JDBC metadata store. | string | 1.6.0 | | kyuubi.metadata.store.jdbc.priority.enabled | false | Whether to enable the priority scheduling for batch impl v2. When false, ignore kyuubi.batch.priority and use the FIFO ordering strategy for batch job scheduling. Note: this feature may cause significant performance issues when using MySQL 5.7 as the metastore backend due to the lack of support for mixed order index. See more details at KYUUBI #5329. | boolean | 1.8.0 | -| kyuubi.metadata.store.jdbc.url | jdbc:sqlite:kyuubi_state_store.db | The JDBC url for server JDBC metadata store. By default, it is a SQLite database url, and the state information is not shared across kyuubi instances. To enable high availability for multiple kyuubi instances, please specify a production JDBC url. | string | 1.6.0 | +| kyuubi.metadata.store.jdbc.url | jdbc:sqlite:<KYUUBI_HOME>/kyuubi_state_store.db | The JDBC url for server JDBC metadata store. By default, it is a SQLite database url, and the state information is not shared across Kyuubi instances. To enable high availability for multiple kyuubi instances, please specify a production JDBC url. Note: this value support the variables substitution: ``. | string | 1.6.0 | | kyuubi.metadata.store.jdbc.user || The username for server JDBC metadata store. | string | 1.6.0 | ### Metrics @@ -381,28 +396,31 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.operation.result.arrow.timestampAsString | false | When true, arrow-based rowsets will convert columns of type timestamp to strings for transmission. | boolean | 1.7.0 | | kyuubi.operation.result.format | thrift | Specify the result format, available configs are:
    • THRIFT: the result will convert to TRow at the engine driver side.
    • ARROW: the result will be encoded as Arrow at the executor side before collecting by the driver, and deserialized at the client side. note that it only takes effect for kyuubi-hive-jdbc clients now.
    | string | 1.7.0 | | kyuubi.operation.result.max.rows | 0 | Max rows of Spark query results. Rows exceeding the limit would be ignored. By setting this value to 0 to disable the max rows limit. | int | 1.6.0 | +| kyuubi.operation.result.saveToFile.dir | /tmp/kyuubi/tmp_kyuubi_result | The Spark query result save dir, it should be a public accessible to every engine. Results are saved in ORC format, and the directory structure is `/OPERATION_RESULT_SAVE_TO_FILE_DIR/engineId/sessionId/statementId`. Each query result will delete when query finished. | string | 1.9.0 | +| kyuubi.operation.result.saveToFile.enabled | false | The switch for Spark query result save to file. | boolean | 1.9.0 | +| kyuubi.operation.result.saveToFile.minSize | 209715200 | The minSize of Spark result save to file, default value is 200 MB.we use spark's `EstimationUtils#getSizePerRowestimate` to estimate the output size of the execution plan. | long | 1.9.0 | | kyuubi.operation.scheduler.pool | <undefined> | The scheduler pool of job. Note that, this config should be used after changing Spark config spark.scheduler.mode=FAIR. | string | 1.1.1 | | kyuubi.operation.spark.listener.enabled | true | When set to true, Spark engine registers an SQLOperationListener before executing the statement, logging a few summary statistics when each stage completes. | boolean | 1.6.0 | | kyuubi.operation.status.polling.timeout | PT5S | Timeout(ms) for long polling asynchronous running sql query's status | duration | 1.0.0 | ### Server -| Key | Default | Meaning | Type | Since | -|----------------------------------------------------------|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| -| kyuubi.server.administrators || Comma-separated list of Kyuubi service administrators. We use this config to grant admin permission to any service accounts. | set | 1.8.0 | -| kyuubi.server.info.provider | ENGINE | The server information provider name, some clients may rely on this information to check the server compatibilities and functionalities.
  • SERVER: Return Kyuubi server information.
  • ENGINE: Return Kyuubi engine information.
  • | string | 1.6.1 | -| kyuubi.server.limit.batch.connections.per.ipaddress | <undefined> | Maximum kyuubi server batch connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.batch.connections.per.user | <undefined> | Maximum kyuubi server batch connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.batch.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server batch connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.client.fetch.max.rows | <undefined> | Max rows limit for getting result row set operation. If the max rows specified by client-side is larger than the limit, request will fail directly. | int | 1.8.0 | -| kyuubi.server.limit.connections.per.ipaddress | <undefined> | Maximum kyuubi server connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.per.user | <undefined> | Maximum kyuubi server connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.user.deny.list || The user in the deny list will be denied to connect to kyuubi server, if the user has configured both user.unlimited.list and user.deny.list, the priority of the latter is higher. | set | 1.8.0 | -| kyuubi.server.limit.connections.user.unlimited.list || The maximum connections of the user in the white list will not be limited. | set | 1.7.0 | -| kyuubi.server.name | <undefined> | The name of Kyuubi Server. | string | 1.5.0 | -| kyuubi.server.periodicGC.interval | PT30M | How often to trigger a garbage collection. | duration | 1.7.0 | -| kyuubi.server.redaction.regex | <undefined> | Regex to decide which Kyuubi contain sensitive information. When this regex matches a property key or value, the value is redacted from the various logs. || 1.6.0 | +| Key | Default | Meaning | Type | Since | +|----------------------------------------------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| +| kyuubi.server.administrators || Comma-separated list of Kyuubi service administrators. We use this config to grant admin permission to any service accounts when security mechanism is enabled. Note, when kyuubi.authentication is configured to NOSASL or NONE, everyone is treated as administrator. | set | 1.8.0 | +| kyuubi.server.info.provider | ENGINE | The server information provider name, some clients may rely on this information to check the server compatibilities and functionalities.
  • SERVER: Return Kyuubi server information.
  • ENGINE: Return Kyuubi engine information.
  • | string | 1.6.1 | +| kyuubi.server.limit.batch.connections.per.ipaddress | <undefined> | Maximum kyuubi server batch connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.batch.connections.per.user | <undefined> | Maximum kyuubi server batch connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.batch.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server batch connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.client.fetch.max.rows | <undefined> | Max rows limit for getting result row set operation. If the max rows specified by client-side is larger than the limit, request will fail directly. | int | 1.8.0 | +| kyuubi.server.limit.connections.per.ipaddress | <undefined> | Maximum kyuubi server connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.per.user | <undefined> | Maximum kyuubi server connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.user.deny.list || The user in the deny list will be denied to connect to kyuubi server, if the user has configured both user.unlimited.list and user.deny.list, the priority of the latter is higher. | set | 1.8.0 | +| kyuubi.server.limit.connections.user.unlimited.list || The maximum connections of the user in the white list will not be limited. | set | 1.7.0 | +| kyuubi.server.name | <undefined> | The name of Kyuubi Server. | string | 1.5.0 | +| kyuubi.server.periodicGC.interval | PT30M | How often to trigger a garbage collection. | duration | 1.7.0 | +| kyuubi.server.redaction.regex | <undefined> | Regex to decide which Kyuubi contain sensitive information. When this regex matches a property key or value, the value is redacted from the various logs. || 1.6.0 | ### Session @@ -410,17 +428,18 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co |------------------------------------------------------|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------| | kyuubi.session.check.interval | PT5M | The check interval for session timeout. | duration | 1.0.0 | | kyuubi.session.close.on.disconnect | true | Session will be closed when client disconnects from kyuubi gateway. Set this to false to have session outlive its parent connection. | boolean | 1.8.0 | -| kyuubi.session.conf.advisor | <undefined> | A config advisor plugin for Kyuubi Server. This plugin can provide some custom configs for different users or session configs and overwrite the session configs before opening a new session. This config value should be a subclass of `org.apache.kyuubi.plugin.SessionConfAdvisor` which has a zero-arg constructor. | string | 1.5.0 | +| kyuubi.session.conf.advisor | <undefined> | A config advisor plugin for Kyuubi Server. This plugin can provide a list of custom configs for different users or session configs and overwrite the session configs before opening a new session. This config value should be a subclass of `org.apache.kyuubi.plugin.SessionConfAdvisor` which has a zero-arg constructor. | seq | 1.5.0 | | kyuubi.session.conf.file.reload.interval | PT10M | When `FileSessionConfAdvisor` is used, this configuration defines the expired time of `$KYUUBI_CONF_DIR/kyuubi-session-.conf` in the cache. After exceeding this value, the file will be reloaded. | duration | 1.7.0 | | kyuubi.session.conf.ignore.list || A comma-separated list of ignored keys. If the client connection contains any of them, the key and the corresponding value will be removed silently during engine bootstrap and connection setup. Note that this rule is for server-side protection defined via administrators to prevent some essential configs from tampering but will not forbid users to set dynamic configurations via SET syntax. | set | 1.2.0 | | kyuubi.session.conf.profile | <undefined> | Specify a profile to load session-level configurations from `$KYUUBI_CONF_DIR/kyuubi-session-.conf`. This configuration will be ignored if the file does not exist. This configuration only takes effect when `kyuubi.session.conf.advisor` is set as `org.apache.kyuubi.session.FileSessionConfAdvisor`. | string | 1.7.0 | | kyuubi.session.conf.restrict.list || A comma-separated list of restricted keys. If the client connection contains any of them, the connection will be rejected explicitly during engine bootstrap and connection setup. Note that this rule is for server-side protection defined via administrators to prevent some essential configs from tampering but will not forbid users to set dynamic configurations via SET syntax. | set | 1.2.0 | -| kyuubi.session.engine.alive.max.failures | 3 | The maximum number of failures allowed for the engine. | int | 1.8.0 | +| kyuubi.session.engine.alive.max.failures | 3 | The maximum number of failures allowed for the engine. | int | 1.8.1 | | kyuubi.session.engine.alive.probe.enabled | false | Whether to enable the engine alive probe, it true, we will create a companion thrift client that keeps sending simple requests to check whether the engine is alive. | boolean | 1.6.0 | | kyuubi.session.engine.alive.probe.interval | PT10S | The interval for engine alive probe. | duration | 1.6.0 | | kyuubi.session.engine.alive.timeout | PT2M | The timeout for engine alive. If there is no alive probe success in the last timeout window, the engine will be marked as no-alive. | duration | 1.6.0 | | kyuubi.session.engine.check.interval | PT1M | The check interval for engine timeout | duration | 1.0.0 | | kyuubi.session.engine.flink.fetch.timeout | <undefined> | Result fetch timeout for Flink engine. If the timeout is reached, the result fetch would be stopped and the current fetched would be returned. If no data are fetched, a TimeoutException would be thrown. | duration | 1.8.0 | +| kyuubi.session.engine.flink.initialize.sql || The initialize sql for Flink session. It fallback to `kyuubi.engine.session.initialize.sql` | seq | 1.8.1 | | kyuubi.session.engine.flink.main.resource | <undefined> | The package used to create Flink SQL engine remote job. If it is undefined, Kyuubi will use the default | string | 1.4.0 | | kyuubi.session.engine.flink.max.rows | 1000000 | Max rows of Flink query results. For batch queries, rows exceeding the limit would be ignored. For streaming queries, the query would be canceled if the limit is reached. | int | 1.5.0 | | kyuubi.session.engine.hive.main.resource | <undefined> | The package used to create Hive engine remote job. If it is undefined, Kyuubi will use the default | string | 1.6.0 | @@ -430,8 +449,10 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.session.engine.log.timeout | PT24H | If we use Spark as the engine then the session submit log is the console output of spark-submit. We will retain the session submit log until over the config value. | duration | 1.1.0 | | kyuubi.session.engine.login.timeout | PT15S | The timeout of creating the connection to remote sql query engine | duration | 1.0.0 | | kyuubi.session.engine.open.max.attempts | 9 | The number of times an open engine will retry when encountering a special error. | int | 1.7.0 | +| kyuubi.session.engine.open.onFailure | RETRY | The behavior when opening engine failed:
    • RETRY: retry to open engine for kyuubi.session.engine.open.max.attempts times.
    • DEREGISTER_IMMEDIATELY: deregister the engine immediately.
    • DEREGISTER_AFTER_RETRY: deregister the engine after retry to open engine for kyuubi.session.engine.open.max.attempts times.
    | string | 1.8.1 | | kyuubi.session.engine.open.retry.wait | PT10S | How long to wait before retrying to open the engine after failure. | duration | 1.7.0 | | kyuubi.session.engine.share.level | USER | (deprecated) - Using kyuubi.engine.share.level instead | string | 1.0.0 | +| kyuubi.session.engine.spark.initialize.sql || The initialize sql for Spark session. It fallback to `kyuubi.engine.session.initialize.sql` | seq | 1.8.1 | | kyuubi.session.engine.spark.main.resource | <undefined> | The package used to create Spark SQL engine remote application. If it is undefined, Kyuubi will use the default | string | 1.0.0 | | kyuubi.session.engine.spark.max.initial.wait | PT1M | Max wait time for the initial connection to Spark engine. The engine will self-terminate no new incoming connection is established within this time. This setting only applies at the CONNECTION share level. 0 or negative means not to self-terminate. | duration | 1.8.0 | | kyuubi.session.engine.spark.max.lifetime | PT0S | Max lifetime for Spark engine, the engine will self-terminate when it reaches the end of life. 0 or negative means not to self-terminate. | duration | 1.6.0 | @@ -451,6 +472,7 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.session.idle.timeout | PT6H | session idle timeout, it will be closed when it's not accessed for this duration | duration | 1.2.0 | | kyuubi.session.local.dir.allow.list || The local dir list that are allowed to access by the kyuubi session application. End-users might set some parameters such as `spark.files` and it will upload some local files when launching the kyuubi engine, if the local dir allow list is defined, kyuubi will check whether the path to upload is in the allow list. Note that, if it is empty, there is no limitation for that. And please use absolute paths. | set | 1.6.0 | | kyuubi.session.name | <undefined> | A human readable name of the session and we use empty string by default. This name will be recorded in the event. Note that, we only apply this value from session conf. | string | 1.4.0 | +| kyuubi.session.proxy.user | <undefined> | An alternative to hive.server2.proxy.user. The current behavior is consistent with hive.server2.proxy.user and now only takes effect in RESTFul API. When both parameters are set, kyuubi.session.proxy.user takes precedence. | string | 1.9.0 | | kyuubi.session.timeout | PT6H | (deprecated)session timeout, it will be closed when it's not accessed for this duration | duration | 1.0.0 | | kyuubi.session.user.sign.enabled | false | Whether to verify the integrity of session user name on the engine side, e.g. Authz plugin in Spark. | boolean | 1.7.0 | @@ -470,19 +492,19 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co ### Zookeeper -| Key | Default | Meaning | Type | Since | -|--------------------------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------| -| kyuubi.zookeeper.embedded.client.port | 2181 | clientPort for the embedded ZooKeeper server to listen for client connections, a client here could be Kyuubi server, engine, and JDBC client | int | 1.2.0 | -| kyuubi.zookeeper.embedded.client.port.address | <undefined> | clientPortAddress for the embedded ZooKeeper server to | string | 1.2.0 | -| kyuubi.zookeeper.embedded.client.use.hostname | false | When true, embedded Zookeeper prefer to bind hostname, otherwise, ip address. | boolean | 1.7.2 | -| kyuubi.zookeeper.embedded.data.dir | embedded_zookeeper | dataDir for the embedded zookeeper server where stores the in-memory database snapshots and, unless specified otherwise, the transaction log of updates to the database. | string | 1.2.0 | -| kyuubi.zookeeper.embedded.data.log.dir | embedded_zookeeper | dataLogDir for the embedded ZooKeeper server where writes the transaction log . | string | 1.2.0 | -| kyuubi.zookeeper.embedded.directory | embedded_zookeeper | The temporary directory for the embedded ZooKeeper server | string | 1.0.0 | -| kyuubi.zookeeper.embedded.max.client.connections | 120 | maxClientCnxns for the embedded ZooKeeper server to limit the number of concurrent connections of a single client identified by IP address | int | 1.2.0 | -| kyuubi.zookeeper.embedded.max.session.timeout | 60000 | maxSessionTimeout in milliseconds for the embedded ZooKeeper server will allow the client to negotiate. Defaults to 20 times the tickTime | int | 1.2.0 | -| kyuubi.zookeeper.embedded.min.session.timeout | 6000 | minSessionTimeout in milliseconds for the embedded ZooKeeper server will allow the client to negotiate. Defaults to 2 times the tickTime | int | 1.2.0 | -| kyuubi.zookeeper.embedded.port | 2181 | The port of the embedded ZooKeeper server | int | 1.0.0 | -| kyuubi.zookeeper.embedded.tick.time | 3000 | tickTime in milliseconds for the embedded ZooKeeper server | int | 1.2.0 | +| Key | Default | Meaning | Type | Since | +|--------------------------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------| +| kyuubi.zookeeper.embedded.client.port | 2181 | clientPort for the embedded ZooKeeper server to listen for client connections, a client here could be Kyuubi server, engine, and JDBC client | int | 1.2.0 | +| kyuubi.zookeeper.embedded.client.port.address | <undefined> | clientPortAddress for the embedded ZooKeeper server to | string | 1.2.0 | +| kyuubi.zookeeper.embedded.client.use.hostname | false | When true, embedded Zookeeper prefer to bind hostname, otherwise, ip address. | boolean | 1.7.2 | +| kyuubi.zookeeper.embedded.data.dir | embedded_zookeeper | dataDir for the embedded zookeeper server where stores the in-memory database snapshots and, unless specified otherwise, the transaction log of updates to the database. If it is a relative path, it is resolved relative to KYUUBI_HOME. | string | 1.2.0 | +| kyuubi.zookeeper.embedded.data.log.dir | embedded_zookeeper | dataLogDir for the embedded ZooKeeper server where writes the transaction log. If it is a relative path, it is resolved relative to KYUUBI_HOME. | string | 1.2.0 | +| kyuubi.zookeeper.embedded.directory | embedded_zookeeper | The temporary directory for the embedded ZooKeeper server. If it is a relative path, it is resolved relative to KYUUBI_HOME. | string | 1.0.0 | +| kyuubi.zookeeper.embedded.max.client.connections | 120 | maxClientCnxns for the embedded ZooKeeper server to limit the number of concurrent connections of a single client identified by IP address | int | 1.2.0 | +| kyuubi.zookeeper.embedded.max.session.timeout | 60000 | maxSessionTimeout in milliseconds for the embedded ZooKeeper server will allow the client to negotiate. Defaults to 20 times the tickTime | int | 1.2.0 | +| kyuubi.zookeeper.embedded.min.session.timeout | 6000 | minSessionTimeout in milliseconds for the embedded ZooKeeper server will allow the client to negotiate. Defaults to 2 times the tickTime | int | 1.2.0 | +| kyuubi.zookeeper.embedded.port | 2181 | The port of the embedded ZooKeeper server | int | 1.0.0 | +| kyuubi.zookeeper.embedded.tick.time | 3000 | tickTime in milliseconds for the embedded ZooKeeper server | int | 1.2.0 | ## Spark Configurations @@ -496,7 +518,11 @@ Setting them in `$KYUUBI_HOME/conf/kyuubi-defaults.conf` supplies with default v ### Via JDBC Connection URL -Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: ```jdbc:hive2://localhost:10009/default;#spark.sql.shuffle.partitions=2;spark.executor.memory=5g``` +Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: + +``` +jdbc:hive2://localhost:10009/default;#spark.sql.shuffle.partitions=2;spark.executor.memory=5g +``` - **Runtime SQL Configuration** - For [Runtime SQL Configurations](https://spark.apache.org/docs/latest/configuration.html#runtime-sql-configuration), they will take affect every time @@ -528,7 +554,11 @@ The below options in `kyuubi-defaults.conf` will set `parallelism.default: 2` an ### Via JDBC Connection URL -Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: ```jdbc:hive2://localhost:10009/default;#parallelism.default=2;taskmanager.memory.process.size=5g``` +Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: + +``` +jdbc:hive2://localhost:10009/default;#flink.parallelism.default=2;flink.taskmanager.memory.process.size=5g +``` ### Via SET Statements @@ -555,7 +585,11 @@ The below options in `kyuubi-defaults.conf` will set `query_max_stage_count: 500 ### Via JDBC Connection URL -Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: ```jdbc:hive2://localhost:10009/default;#trino.query_max_stage_count=500;trino.parse_decimal_literals_as_double=true``` +Setting them in the JDBC Connection URL supplies session-specific for each SQL engine. For example: + +``` +jdbc:hive2://localhost:10009/default;#trino.query_max_stage_count=500;trino.parse_decimal_literals_as_double=true +``` ### Via SET Statements diff --git a/docs/connector/spark/delta_lake_with_azure_blob.rst b/docs/connector/spark/delta_lake_with_azure_blob.rst index 1d7cab048b6..bdca3f5dfee 100644 --- a/docs/connector/spark/delta_lake_with_azure_blob.rst +++ b/docs/connector/spark/delta_lake_with_azure_blob.rst @@ -171,6 +171,7 @@ Enter the ./kyuubi/conf directory Add the following content: .. code-block:: properties + spark.master spark://:7077 kyuubi.authentication NONE kyuubi.frontend.bind.host @@ -220,6 +221,7 @@ Create Table ************ .. code-block:: sql + -- Create or replace table with path CREATE OR REPLACE TABLE delta.`wasbs://1000@azure_account.blob.core.windows.net/alexDemo20211129` ( date DATE, @@ -276,6 +278,7 @@ Overwrite Mode Result: .. code-block:: text + +-------------+----------+------------+---------------+ | date | eventId | eventType | data | +-------------+----------+------------+---------------+ @@ -287,6 +290,7 @@ Delete Table Data ***************** .. code-block:: sql + DELETE FROM delta.`wasbs://1000@azure_account.blob.core.windows.net/alexDemo20211129` WHERE eventId = 002; diff --git a/docs/connector/spark/hudi.rst b/docs/connector/spark/hudi.rst index 045e75146f0..3ccd1f93b18 100644 --- a/docs/connector/spark/hudi.rst +++ b/docs/connector/spark/hudi.rst @@ -60,6 +60,7 @@ Configurations To activate functionality of Hudi, we can set the following configurations: .. code-block:: properties + # Spark 3.2 spark.serializer=org.apache.spark.serializer.KryoSerializer spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension diff --git a/docs/contributing/code/building.md b/docs/contributing/code/building.md index 8c5c5aeec60..bfa6a46caed 100644 --- a/docs/contributing/code/building.md +++ b/docs/contributing/code/building.md @@ -63,9 +63,24 @@ Since v1.1.0, Kyuubi support building with different Spark profiles, | Profile | Default | Since | |-------------|---------|-------| -| -Pspark-3.1 | No | 1.1.0 | -| -Pspark-3.2 | No | 1.4.0 | -| -Pspark-3.3 | Yes | 1.6.0 | +| -Pspark-3.1 | | 1.1.0 | +| -Pspark-3.2 | | 1.4.0 | +| -Pspark-3.3 | | 1.6.0 | +| -Pspark-3.4 | โœ“ | 1.8.0 | +| -Pspark-3.5 | | 1.8.0 | + +## Building Kyuubi Against Different Scala Versions + +Since v1.8.0, Kyuubi support building with different Scala profile. Currently, Kyuubi supports building with Scala 2.12 and 2.13, while Scala 2.12 by default. + +| Profile | Default | Since | +|--------------|---------|-------| +| (Scala 2.12) | โœ“ | - | +| -Pscala-2.13 | | 1.8.0 | + +Please activate `scala-2.13` profile when Scala 2.13 support is needed. The GA tests have covered integration test with the Kyuubi server, engines and related plugins, while the Flink engine and it's integration tests are not included for the reason that Flink does not support Scala 2.13 yet and will pull out client support for Scala. + +For the Scala version for Spark engines, the server will look up the `SPARK_SCALA_VERSION` system environment variable first, and then the Scala version of the server compiled with if the former one not set. For the Scala version for other engines, the server will use the Scala version of the server compiled with. ## Building With Apache dlcdn Site diff --git a/docs/contributing/code/style.rst b/docs/contributing/code/style.rst index d967e895971..fced388a3ed 100644 --- a/docs/contributing/code/style.rst +++ b/docs/contributing/code/style.rst @@ -35,5 +35,11 @@ Java Coding Style Guide Kyuubi adopts the `Google Java style`_ for java codes. +Documentation Style Guide +------------------------- + +Kyuubi adopts the `Documentation Style Guide`_ for documentation. + .. _Databricks Scala Coding Style Guide: https://github.com/databricks/scala-style-guide -.. _Google Java style: https://google.github.io/styleguide/javaguide.html \ No newline at end of file +.. _Google Java style: https://google.github.io/styleguide/javaguide.html +.. _Documentation Style Guide: ../doc/style.html \ No newline at end of file diff --git a/docs/deployment/migration-guide.md b/docs/deployment/migration-guide.md index 58df0fcc629..9a099d58508 100644 --- a/docs/deployment/migration-guide.md +++ b/docs/deployment/migration-guide.md @@ -17,13 +17,24 @@ # Kyuubi Migration Guide +## Upgrading from Kyuubi 1.8 to 1.9 + +* Since Kyuubi 1.9.0, `kyuubi.session.conf.advisor` can be set as a sequence, Kyuubi supported chaining SessionConfAdvisors. + +## Upgrading from Kyuubi 1.8.0 to 1.8.1 + +* Since Kyuubi 1.8.1, for `DELETE /batches/${batchId}`, `hive.server2.proxy.user` is not needed in the request parameters. +* Since Kyuubi 1.8.1, the default SQLite file `kyuubi_state_store.db` for Metadata store is located under `$KYUUBI_HOME` instead of `$PWD`. To restore previous behavior, set `kyuubi.metadata.store.jdbc.url` to `jdbc:sqlite:kyuubi_state_store.db`. + ## Upgrading from Kyuubi 1.7 to 1.8 * Since Kyuubi 1.8, SQLite is added and becomes the default database type of Kyuubi metastore, as Derby has been deprecated. Both Derby and SQLite are mainly for testing purposes, and they're not supposed to be used in production. To restore previous behavior, set `kyuubi.metadata.store.jdbc.database.type=DERBY` and `kyuubi.metadata.store.jdbc.url=jdbc:derby:memory:kyuubi_state_store_db;create=true`. - +* Since Kyuubi 1.8, if the directory of the embedded zookeeper configuration (`kyuubi.zookeeper.embedded.directory` + & `kyuubi.zookeeper.embedded.data.dir` & `kyuubi.zookeeper.embedded.data.log.dir`) is a relative path, it is resolved + relative to `$KYUUBI_HOME` instead of `$PWD`. * Since Kyuubi 1.8, PROMETHEUS is changed as the default metrics reporter. To restore previous behavior, set `kyuubi.metrics.reporters=JSON`. diff --git a/docs/deployment/spark/gluten.md b/docs/deployment/spark/gluten.md new file mode 100644 index 00000000000..8f6bcdef7af --- /dev/null +++ b/docs/deployment/spark/gluten.md @@ -0,0 +1,55 @@ + + + +# Gluten + +Gluten is a Spark plugin developed by Intel, designed to accelerate Apache Spark with native libraries. Currently, only CentOS 7/8 and Ubuntu 20.04/22.04, along with Spark 3.2/3.3/3.4, are supported. Users can employ the following methods to utilize the Gluten with Velox native libraries. + +## Building(with velox Backend) + +### Build gluten velox backend package + +Git clone gluten project, use gluten build script `buildbundle-veloxbe.sh`, and target package is in `/path/to/gluten/package/target/` + +```bash +git clone https://github.com/oap-project/gluten.git +cd /path/to/gluten + +## The script builds two jars for spark 3.2.x, 3.3.x, and 3.4.x. +./dev/buildbundle-veloxbe.sh +``` + +## Usage + +You can use Gluten to accelerate Spark by following steps. + +### Installing + +add gluten jar: `copy /path/to/gluten/package/target/gluten-velox-bundle-spark3.x_2.12-*.jar $SPARK_HOME/jars/` or specified to `spark.jars` configuration + +### Configure + +add config into `spark-defaults.conf`: + +```properties +spark.plugins=io.glutenproject.GlutenPlugin +spark.memory.offHeap.size=20g +spark.memory.offHeap.enabled=true +spark.shuffle.manager=org.apache.spark.shuffle.sort.ColumnarShuffleManager +``` + diff --git a/docs/deployment/spark/index.rst b/docs/deployment/spark/index.rst index 0d75c506390..acaaa6ec511 100644 --- a/docs/deployment/spark/index.rst +++ b/docs/deployment/spark/index.rst @@ -30,3 +30,4 @@ Even if you don't use Kyuubi, as a simple Spark user, I'm sure you'll find the n dynamic_allocation aqe incremental_collection + gluten diff --git a/docs/extensions/engines/spark/rules.md b/docs/extensions/engines/spark/rules.md index 4614f52440a..55357e46d26 100644 --- a/docs/extensions/engines/spark/rules.md +++ b/docs/extensions/engines/spark/rules.md @@ -92,4 +92,5 @@ Kyuubi provides some configs to make these feature easy to use. | spark.sql.finalWriteStageExecutorMemory | fallback spark.executor.memory | Specify the executor on heap memory request for final write stage. It would be passed to the RDD resource profile. | 1.8.0 | | spark.sql.finalWriteStageExecutorMemoryOverhead | fallback spark.executor.memoryOverhead | Specify the executor memory overhead request for final write stage. It would be passed to the RDD resource profile. | 1.8.0 | | spark.sql.finalWriteStageExecutorOffHeapMemory | NONE | Specify the executor off heap memory request for final write stage. It would be passed to the RDD resource profile. | 1.8.0 | +| spark.sql.execution.scriptTransformation.enabled | true | When false, script transformation is not allowed. | 1.9.0 | diff --git a/docs/imgs/kyuubi_layers.drawio b/docs/imgs/kyuubi_layers.drawio index 2c3348ed4b8..95dc80ef9a2 100644 --- a/docs/imgs/kyuubi_layers.drawio +++ b/docs/imgs/kyuubi_layers.drawio @@ -1 +1,157 @@ -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 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/imgs/kyuubi_layers.drawio.png b/docs/imgs/kyuubi_layers.drawio.png index ebc6b74a323..f3a8c059989 100644 Binary files a/docs/imgs/kyuubi_layers.drawio.png and b/docs/imgs/kyuubi_layers.drawio.png differ diff --git a/docs/imgs/ui/engine_ui.png b/docs/imgs/ui/engine_ui.png new file mode 100644 index 00000000000..a0044b4dc1d Binary files /dev/null and b/docs/imgs/ui/engine_ui.png differ diff --git a/docs/index.rst b/docs/index.rst index e86041ffc0d..ab9cf271fc2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,8 @@ Apache `Spark `_, `Flink `_, `Doris `_, `Hive `_, -and `Trino `_, etc, to query massive datasets distributed +`Trino `_, +and `StarRocks `_, etc., to query massive datasets distributed over fleets of machines from heterogeneous data sources. The Kyuubi Server lane of the below swimlane divides our prospective users into @@ -138,7 +139,7 @@ by professionals on the Kyuubi server side. It is suitable for the following sce in your data lake in cloud storage or an on-prem HDFS cluster. - Lakehouse formation and analytics - - Easily build an ACID table storage layer via Hudi, Iceberg, or/and Delta Lake. + - Easily build an ACID table storage layer via Hudi, Iceberg, Delta Lake or/and Paimon. - Logical data warehouse - Provide a relational abstraction on top of disparate data without ETL jobs, diff --git a/docs/quick_start/quick_start.rst b/docs/quick_start/quick_start.rst index 9d0a7d30ccf..85a215aad8e 100644 --- a/docs/quick_start/quick_start.rst +++ b/docs/quick_start/quick_start.rst @@ -36,23 +36,23 @@ For quick start deployment, we need to prepare the following stuffs: These essential components are JVM-based applications. So, the JRE needs to be pre-installed and the ``JAVA_HOME`` is correctly set to each component. - ================ ============ =============== =========================================== - Component Role Version Remarks - ================ ============ =============== =========================================== - **Java** JRE 8/11/17 Officially released against JDK8 - **Kyuubi** Gateway \ |release| \ - Kyuubi Server - Engine lib - Kyuubi Engine - Beeline - Kyuubi Hive Beeline - **Spark** Engine >=3.1 A Spark distribution - **Flink** Engine 1.16/1.17/1.18 A Flink distribution - **Trino** Engine >=363 A Trino cluster - **Doris** Engine N/A A Doris cluster - **Hive** Engine - 3.1.x - A Hive distribution - Metastore - N/A - An optional and external metadata store, - whose version is decided by engines + ================ ============ ==================== =========================================== + Component Role Version Remarks + ================ ============ ==================== =========================================== + **Java** JRE 8/11/17 Officially released against JDK8 + **Kyuubi** Gateway \ |release| \ - Kyuubi Server + Engine lib - Kyuubi Engine + Beeline - Kyuubi Hive Beeline + **Spark** Engine 3.1 to 3.5 A Spark distribution + **Flink** Engine 1.16/1.17/1.18 A Flink distribution + **Trino** Engine >=363 A Trino cluster + **Doris** Engine N/A A Doris cluster + **Hive** Engine - 2.1-cdh6/2.3/3.1 - A Hive distribution + Metastore - N/A - An optional and external metadata store, + whose version is decided by engines **Zookeeper** HA >=3.4.x - **Disk** Storage N/A N/A - ================ ============ =============== =========================================== + **Disk** Storage N/A N/A + ================ ============ ==================== =========================================== The other internal or external parts listed in the above sheet can be used individually or all together. For example, you can use Kyuubi, Spark and Flink to build a streaming diff --git a/docs/requirements.txt b/docs/requirements.txt index 8e1f5c47119..b2f9efc4a4e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -17,12 +17,12 @@ # under the License. # -markdown==3.4.1 +markdown==3.5.1 recommonmark==0.7.1 -sphinx==4.5.0 -sphinx-book-theme==0.3.3 +sphinx==7.2.6 +sphinx-book-theme==1.1.0 sphinx-markdown-tables==0.0.17 -sphinx-notfound-page==0.8.3 +sphinx-notfound-page==1.0.0 sphinx-togglebutton===0.3.2 sphinxemoji===0.2.0 sphinx-copybutton===0.5.2 diff --git a/docs/tools/kyuubi-admin.rst b/docs/tools/kyuubi-admin.rst index bd37f7e684f..29149e92f5f 100644 --- a/docs/tools/kyuubi-admin.rst +++ b/docs/tools/kyuubi-admin.rst @@ -99,8 +99,6 @@ Usage: ``bin/kyuubi-admin list engine [options]`` - The subdomain for the share level of an engine. If not specified, it will read the configuration item kyuubi.engine.share.level.subdomain from kyuubi-defaults.conf. * - --hs2ProxyUser - The proxy user to impersonate. When specified, it will list engines for the hs2ProxyUser. - * - -a --all - - All the engine. .. _list_server: diff --git a/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala b/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala index 62aa88b9861..3dda669a8a3 100644 --- a/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala @@ -44,7 +44,7 @@ object KyuubiSparkSQLCommonExtension { extensions.injectQueryStagePrepRule(_ => InsertShuffleNodeBeforeJoin) - extensions.injectPostHocResolutionRule(session => MarkNumOutputColumnsRule(session)) + extensions.injectPostHocResolutionRule(MarkNumOutputColumnsRule(_)) extensions.injectQueryStagePrepRule(FinalStageConfigIsolation(_)) } } diff --git a/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala b/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala index f952b56f387..f61eb731e58 100644 --- a/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-1/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala @@ -20,7 +20,7 @@ package org.apache.kyuubi.sql import org.apache.spark.sql.SparkSessionExtensions import org.apache.kyuubi.sql.sqlclassification.KyuubiSqlClassification -import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, MaxScanStrategy} +import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, KyuubiUnsupportedOperationsCheck, MaxScanStrategy} // scalastyle:off line.size.limit /** @@ -39,6 +39,7 @@ class KyuubiSparkSQLExtension extends (SparkSessionExtensions => Unit) { extensions.injectPostHocResolutionRule(DropIgnoreNonexistent) // watchdog extension + extensions.injectCheckRule(_ => KyuubiUnsupportedOperationsCheck) extensions.injectOptimizerRule(ForcedMaxOutputRowsRule) extensions.injectPlannerStrategy(MaxScanStrategy) } diff --git a/extensions/spark/kyuubi-extension-spark-3-2/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala b/extensions/spark/kyuubi-extension-spark-3-2/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala index 97e77704293..9a0f5b1bb6b 100644 --- a/extensions/spark/kyuubi-extension-spark-3-2/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-2/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.sql import org.apache.spark.sql.SparkSessionExtensions -import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, MaxScanStrategy} +import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, KyuubiUnsupportedOperationsCheck, MaxScanStrategy} // scalastyle:off line.size.limit /** @@ -37,6 +37,7 @@ class KyuubiSparkSQLExtension extends (SparkSessionExtensions => Unit) { extensions.injectPostHocResolutionRule(DropIgnoreNonexistent) // watchdog extension + extensions.injectCheckRule(_ => KyuubiUnsupportedOperationsCheck) extensions.injectOptimizerRule(ForcedMaxOutputRowsRule) extensions.injectPlannerStrategy(MaxScanStrategy) } diff --git a/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala b/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala index 170b5a16509..c001ffc6c3b 100644 --- a/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala @@ -44,7 +44,7 @@ object KyuubiSparkSQLCommonExtension { extensions.injectQueryStagePrepRule(_ => InsertShuffleNodeBeforeJoin) - extensions.injectPostHocResolutionRule(session => MarkNumOutputColumnsRule(session)) + extensions.injectPostHocResolutionRule(MarkNumOutputColumnsRule(_)) extensions.injectQueryStagePrepRule(FinalStageConfigIsolation(_)) } } diff --git a/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala b/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala index 792315d897a..fd11fb5f579 100644 --- a/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-3/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.sql import org.apache.spark.sql.{FinalStageResourceManager, InjectCustomResourceProfile, SparkSessionExtensions} -import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, MaxScanStrategy} +import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, KyuubiUnsupportedOperationsCheck, MaxScanStrategy} // scalastyle:off line.size.limit /** @@ -37,6 +37,7 @@ class KyuubiSparkSQLExtension extends (SparkSessionExtensions => Unit) { extensions.injectPostHocResolutionRule(DropIgnoreNonexistent) // watchdog extension + extensions.injectCheckRule(_ => KyuubiUnsupportedOperationsCheck) extensions.injectOptimizerRule(ForcedMaxOutputRowsRule) extensions.injectPlannerStrategy(MaxScanStrategy) diff --git a/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala index 6f45dae126e..4b16d3e1681 100644 --- a/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala +++ b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala @@ -273,4 +273,11 @@ object KyuubiSQLConf { .version("1.8.0") .stringConf .createOptional + + val SCRIPT_TRANSFORMATION_ENABLED = + buildConf("spark.sql.execution.scriptTransformation.enabled") + .doc("When false, script transformation is not allowed.") + .version("1.9.0") + .booleanConf + .createWithDefault(true) } diff --git a/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala index 792315d897a..fd11fb5f579 100644 --- a/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.sql import org.apache.spark.sql.{FinalStageResourceManager, InjectCustomResourceProfile, SparkSessionExtensions} -import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, MaxScanStrategy} +import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, KyuubiUnsupportedOperationsCheck, MaxScanStrategy} // scalastyle:off line.size.limit /** @@ -37,6 +37,7 @@ class KyuubiSparkSQLExtension extends (SparkSessionExtensions => Unit) { extensions.injectPostHocResolutionRule(DropIgnoreNonexistent) // watchdog extension + extensions.injectCheckRule(_ => KyuubiUnsupportedOperationsCheck) extensions.injectOptimizerRule(ForcedMaxOutputRowsRule) extensions.injectPlannerStrategy(MaxScanStrategy) diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisRowSetHelper.scala b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala similarity index 54% rename from externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisRowSetHelper.scala rename to extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala index a92942cecdf..2b4d3940ada 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisRowSetHelper.scala +++ b/extensions/spark/kyuubi-extension-spark-3-4/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala @@ -14,23 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.kyuubi.engine.jdbc.doris -import org.apache.hive.service.rpc.thrift._ +package org.apache.kyuubi.sql.watchdog -import org.apache.kyuubi.engine.jdbc.schema.RowSetHelper +import org.apache.spark.sql.catalyst.SQLConfHelper +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, ScriptTransformation} -class DorisRowSetHelper extends RowSetHelper { +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} - override def toTinyIntTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = - toIntegerTColumn(rows, ordinal) - - override def toSmallIntTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = - toIntegerTColumn(rows, ordinal) - - override def toTinyIntTColumnValue(row: List[Any], ordinal: Int): TColumnValue = - toIntegerTColumnValue(row, ordinal) - - override def toSmallIntTColumnValue(row: List[Any], ordinal: Int): TColumnValue = - toIntegerTColumnValue(row, ordinal) +object KyuubiUnsupportedOperationsCheck extends (LogicalPlan => Unit) with SQLConfHelper { + override def apply(plan: LogicalPlan): Unit = + conf.getConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED) match { + case false => plan foreach { + case _: ScriptTransformation => + throw new KyuubiSQLExtensionException("Script transformation is not allowed") + case _ => + } + case true => + } } diff --git a/extensions/spark/kyuubi-extension-spark-3-4/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala b/extensions/spark/kyuubi-extension-spark-3-4/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala index a202e813c5e..139efd9ca06 100644 --- a/extensions/spark/kyuubi-extension-spark-3-4/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala +++ b/extensions/spark/kyuubi-extension-spark-3-4/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala @@ -24,7 +24,7 @@ import scala.collection.JavaConverters._ import org.apache.commons.io.FileUtils import org.apache.spark.sql.catalyst.plans.logical.{GlobalLimit, LogicalPlan} -import org.apache.kyuubi.sql.KyuubiSQLConf +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} import org.apache.kyuubi.sql.watchdog.{MaxFileSizeExceedException, MaxPartitionExceedException} trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { @@ -598,4 +598,13 @@ trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { } } } + + test("disable script transformation") { + withSQLConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED.key -> "false") { + val e = intercept[KyuubiSQLExtensionException] { + sql("SELECT TRANSFORM('') USING 'ls /'") + } + assert(e.getMessage == "Script transformation is not allowed") + } + } } diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/DynamicShufflePartitions.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/DynamicShufflePartitions.scala new file mode 100644 index 00000000000..03d93d07680 --- /dev/null +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/DynamicShufflePartitions.scala @@ -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. + */ +package org.apache.kyuubi.sql + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.catalyst.plans.physical.{HashPartitioning, RangePartitioning, RoundRobinPartitioning} +import org.apache.spark.sql.catalyst.rules.Rule +import org.apache.spark.sql.execution.{FileSourceScanExec, SparkPlan} +import org.apache.spark.sql.execution.adaptive.ShuffleQueryStageExec +import org.apache.spark.sql.execution.exchange.{REPARTITION_BY_NUM, ShuffleExchangeExec, ValidateRequirements} +import org.apache.spark.sql.hive.HiveSparkPlanHelper.HiveTableScanExec +import org.apache.spark.sql.internal.SQLConf._ + +import org.apache.kyuubi.sql.KyuubiSQLConf.{DYNAMIC_SHUFFLE_PARTITIONS, DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM} + +/** + * Dynamically adjust the number of shuffle partitions according to the input data size + */ +case class DynamicShufflePartitions(spark: SparkSession) extends Rule[SparkPlan] { + + override def apply(plan: SparkPlan): SparkPlan = { + if (!conf.getConf(DYNAMIC_SHUFFLE_PARTITIONS) || !conf.getConf(ADAPTIVE_EXECUTION_ENABLED)) { + plan + } else { + val maxDynamicShufflePartitions = conf.getConf(DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM) + + def collectScanSizes(plan: SparkPlan): Seq[Long] = plan match { + case FileSourceScanExec(relation, _, _, _, _, _, _, _, _) => + Seq(relation.location.sizeInBytes) + case t: HiveTableScanExec => + t.relation.prunedPartitions match { + case Some(partitions) => Seq(partitions.flatMap(_.stats).map(_.sizeInBytes.toLong).sum) + case None => Seq(t.relation.computeStats().sizeInBytes.toLong) + .filter(_ != conf.defaultSizeInBytes) + } + case stage: ShuffleQueryStageExec if stage.isMaterialized && stage.mapStats.isDefined => + Seq(stage.mapStats.get.bytesByPartitionId.sum) + case p => + p.children.flatMap(collectScanSizes) + } + + val scanSizes = collectScanSizes(plan) + if (scanSizes.isEmpty) { + return plan + } + + val targetSize = conf.getConf(ADVISORY_PARTITION_SIZE_IN_BYTES) + val targetShufflePartitions = Math.min( + Math.max(scanSizes.sum / targetSize + 1, conf.numShufflePartitions).toInt, + maxDynamicShufflePartitions) + + val newPlan = plan transformUp { + case exchange @ ShuffleExchangeExec(outputPartitioning, _, shuffleOrigin, _) + if shuffleOrigin != REPARTITION_BY_NUM => + val newOutPartitioning = outputPartitioning match { + case RoundRobinPartitioning(numPartitions) + if targetShufflePartitions != numPartitions => + Some(RoundRobinPartitioning(targetShufflePartitions)) + case HashPartitioning(expressions, numPartitions) + if targetShufflePartitions != numPartitions => + Some(HashPartitioning(expressions, targetShufflePartitions)) + case RangePartitioning(ordering, numPartitions) + if targetShufflePartitions != numPartitions => + Some(RangePartitioning(ordering, targetShufflePartitions)) + case _ => None + } + if (newOutPartitioning.isDefined) { + exchange.copy(outputPartitioning = newOutPartitioning.get) + } else { + exchange + } + } + + if (ValidateRequirements.validate(newPlan)) { + newPlan + } else { + logInfo("DynamicShufflePartitions rule generated an invalid plan. " + + "Falling back to the original plan.") + plan + } + } + } + +} diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/InferRebalanceAndSortOrders.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/InferRebalanceAndSortOrders.scala index fcbf5c0a122..3b840f2a014 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/InferRebalanceAndSortOrders.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/InferRebalanceAndSortOrders.scala @@ -22,7 +22,7 @@ import scala.annotation.tailrec import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, AttributeSet, Expression, NamedExpression, UnaryExpression} import org.apache.spark.sql.catalyst.planning.ExtractEquiJoinKeys import org.apache.spark.sql.catalyst.plans.{FullOuter, Inner, LeftAnti, LeftOuter, LeftSemi, RightOuter} -import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, LogicalPlan, Project, Sort, SubqueryAlias, View} +import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, Generate, LogicalPlan, Project, Sort, SubqueryAlias, View, Window} /** * Infer the columns for Rebalance and Sort to improve the compression ratio. @@ -96,6 +96,12 @@ object InferRebalanceAndSortOrders { case f: Filter => candidateKeys(f.child, output) case s: SubqueryAlias => candidateKeys(s.child, output) case v: View => candidateKeys(v.child, output) + case g: Generate => candidateKeys(g.child, AttributeSet(g.requiredChildOutput)) + case w: Window => + val aliasMap = getAliasMap(w.windowExpressions) + Some(( + w.partitionSpec.map(p => aliasMap.getOrElse(p.canonicalized, p)), + w.orderSpec.map(_.child).map(o => aliasMap.getOrElse(o.canonicalized, o)))) case _ => None } diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala index 6f45dae126e..7c4e8d631ef 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala @@ -273,4 +273,27 @@ object KyuubiSQLConf { .version("1.8.0") .stringConf .createOptional + + val DYNAMIC_SHUFFLE_PARTITIONS = + buildConf("spark.sql.optimizer.dynamicShufflePartitions") + .doc("If true, adjust the number of shuffle partitions dynamically based on the job" + + " input size. The new number of partitions is the maximum input size" + + " divided by `spark.sql.adaptive.advisoryPartitionSizeInBytes`.") + .version("1.9.0") + .booleanConf + .createWithDefault(false) + + val DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM = + buildConf("spark.sql.optimizer.dynamicShufflePartitions.maxNum") + .doc("The maximum partition number of DynamicShufflePartitions.") + .version("1.9.0") + .intConf + .createWithDefault(2000) + + val SCRIPT_TRANSFORMATION_ENABLED = + buildConf("spark.sql.execution.scriptTransformation.enabled") + .doc("When false, script transformation is not allowed.") + .version("1.9.0") + .booleanConf + .createWithDefault(true) } diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala index f39ad3cc390..ad95ac4295e 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLCommonExtension.scala @@ -43,6 +43,7 @@ object KyuubiSparkSQLCommonExtension { extensions.injectPostHocResolutionRule(FinalStageConfigIsolationCleanRule) extensions.injectQueryStagePrepRule(_ => InsertShuffleNodeBeforeJoin) + extensions.injectQueryStagePrepRule(DynamicShufflePartitions) extensions.injectQueryStagePrepRule(FinalStageConfigIsolation(_)) } diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala index 792315d897a..fd11fb5f579 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/KyuubiSparkSQLExtension.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.sql import org.apache.spark.sql.{FinalStageResourceManager, InjectCustomResourceProfile, SparkSessionExtensions} -import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, MaxScanStrategy} +import org.apache.kyuubi.sql.watchdog.{ForcedMaxOutputRowsRule, KyuubiUnsupportedOperationsCheck, MaxScanStrategy} // scalastyle:off line.size.limit /** @@ -37,6 +37,7 @@ class KyuubiSparkSQLExtension extends (SparkSessionExtensions => Unit) { extensions.injectPostHocResolutionRule(DropIgnoreNonexistent) // watchdog extension + extensions.injectCheckRule(_ => KyuubiUnsupportedOperationsCheck) extensions.injectOptimizerRule(ForcedMaxOutputRowsRule) extensions.injectPlannerStrategy(MaxScanStrategy) diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala new file mode 100644 index 00000000000..2b4d3940ada --- /dev/null +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala @@ -0,0 +1,35 @@ +/* + * 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.kyuubi.sql.watchdog + +import org.apache.spark.sql.catalyst.SQLConfHelper +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, ScriptTransformation} + +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} + +object KyuubiUnsupportedOperationsCheck extends (LogicalPlan => Unit) with SQLConfHelper { + override def apply(plan: LogicalPlan): Unit = + conf.getConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED) match { + case false => plan foreach { + case _: ScriptTransformation => + throw new KyuubiSQLExtensionException("Script transformation is not allowed") + case _ => + } + case true => + } +} diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/spark/sql/hive/HiveSparkPlanHelper.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/spark/sql/hive/HiveSparkPlanHelper.scala new file mode 100644 index 00000000000..aa9a0459616 --- /dev/null +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/main/scala/org/apache/spark/sql/hive/HiveSparkPlanHelper.scala @@ -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. + */ +package org.apache.spark.sql.hive + +object HiveSparkPlanHelper { + type HiveTableScanExec = org.apache.spark.sql.hive.execution.HiveTableScanExec +} diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/DynamicShufflePartitionsSuite.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/DynamicShufflePartitionsSuite.scala new file mode 100644 index 00000000000..6668675a5f5 --- /dev/null +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/DynamicShufflePartitionsSuite.scala @@ -0,0 +1,140 @@ +/* + * 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.spark.sql + +import org.apache.spark.sql.execution.{CommandResultExec, SparkPlan} +import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanExec, ShuffleQueryStageExec} +import org.apache.spark.sql.execution.exchange.{ENSURE_REQUIREMENTS, ShuffleExchangeExec} +import org.apache.spark.sql.hive.HiveUtils.CONVERT_METASTORE_PARQUET +import org.apache.spark.sql.internal.SQLConf._ + +import org.apache.kyuubi.sql.KyuubiSQLConf.{DYNAMIC_SHUFFLE_PARTITIONS, DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM} + +class DynamicShufflePartitionsSuite extends KyuubiSparkSQLExtensionTest { + + override protected def beforeAll(): Unit = { + super.beforeAll() + setupData() + } + + test("test dynamic shuffle partitions") { + def collectExchanges(plan: SparkPlan): Seq[ShuffleExchangeExec] = { + plan match { + case p: CommandResultExec => collectExchanges(p.commandPhysicalPlan) + case p: AdaptiveSparkPlanExec => collectExchanges(p.finalPhysicalPlan) + case p: ShuffleQueryStageExec => collectExchanges(p.plan) + case p: ShuffleExchangeExec => p +: collectExchanges(p.child) + case p => p.children.flatMap(collectExchanges) + } + } + + // datasource scan + withTable("table1", "table2", "table3") { + sql("create table table1 stored as parquet as select c1, c2 from t1") + sql("create table table2 stored as parquet as select c1, c2 from t2") + sql("create table table3 (c1 int, c2 string) stored as parquet") + sql("ANALYZE TABLE table1 COMPUTE STATISTICS") + sql("ANALYZE TABLE table2 COMPUTE STATISTICS") + + val initialPartitionNum: Int = 2 + Seq(false, true).foreach { dynamicShufflePartitions => + val maxDynamicShufflePartitions = if (dynamicShufflePartitions) { + Seq(8, 2000) + } else { + Seq(2000) + } + maxDynamicShufflePartitions.foreach { maxDynamicShufflePartitionNum => + withSQLConf( + DYNAMIC_SHUFFLE_PARTITIONS.key -> dynamicShufflePartitions.toString, + DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM.key -> maxDynamicShufflePartitionNum.toString, + AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1", + COALESCE_PARTITIONS_INITIAL_PARTITION_NUM.key -> initialPartitionNum.toString, + ADVISORY_PARTITION_SIZE_IN_BYTES.key -> "500") { + val df = sql("insert overwrite table3 " + + " select a.c1 as c1, b.c2 as c2 from table1 a join table2 b on a.c1 = b.c1") + + val exchanges = collectExchanges(df.queryExecution.executedPlan) + val (joinExchanges, rebalanceExchanges) = exchanges + .partition(_.shuffleOrigin == ENSURE_REQUIREMENTS) + // table scan size: 7369 3287 + assert(joinExchanges.size == 2) + if (dynamicShufflePartitions) { + joinExchanges.foreach(e => + assert(e.outputPartitioning.numPartitions + == Math.min(22, maxDynamicShufflePartitionNum))) + } else { + joinExchanges.foreach(e => + assert(e.outputPartitioning.numPartitions == initialPartitionNum)) + } + + assert(rebalanceExchanges.size == 1) + if (dynamicShufflePartitions) { + if (maxDynamicShufflePartitionNum == 8) { + // shuffle query size: 1424 451 + assert(rebalanceExchanges.head.outputPartitioning.numPartitions == + Math.min(4, maxDynamicShufflePartitionNum)) + } else { + // shuffle query size: 2057 664 + assert(rebalanceExchanges.head.outputPartitioning.numPartitions == + Math.min(6, maxDynamicShufflePartitionNum)) + } + } else { + assert( + rebalanceExchanges.head.outputPartitioning.numPartitions == initialPartitionNum) + } + } + + // hive table scan + withSQLConf( + DYNAMIC_SHUFFLE_PARTITIONS.key -> dynamicShufflePartitions.toString, + DYNAMIC_SHUFFLE_PARTITIONS_MAX_NUM.key -> maxDynamicShufflePartitionNum.toString, + AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1", + COALESCE_PARTITIONS_INITIAL_PARTITION_NUM.key -> initialPartitionNum.toString, + ADVISORY_PARTITION_SIZE_IN_BYTES.key -> "500", + CONVERT_METASTORE_PARQUET.key -> "false") { + val df = sql("insert overwrite table3 " + + " select a.c1 as c1, b.c2 as c2 from table1 a join table2 b on a.c1 = b.c1") + + val exchanges = collectExchanges(df.queryExecution.executedPlan) + val (joinExchanges, rebalanceExchanges) = exchanges + .partition(_.shuffleOrigin == ENSURE_REQUIREMENTS) + // table scan size: 7369 3287 + assert(joinExchanges.size == 2) + if (dynamicShufflePartitions) { + joinExchanges.foreach(e => + assert(e.outputPartitioning.numPartitions == + Math.min(22, maxDynamicShufflePartitionNum))) + } else { + joinExchanges.foreach(e => + assert(e.outputPartitioning.numPartitions == initialPartitionNum)) + } + // shuffle query size: 5154 720 + assert(rebalanceExchanges.size == 1) + if (dynamicShufflePartitions) { + assert(rebalanceExchanges.head.outputPartitioning.numPartitions + == Math.min(12, maxDynamicShufflePartitionNum)) + } else { + assert(rebalanceExchanges.head.outputPartitioning.numPartitions == + initialPartitionNum) + } + } + } + } + } + } + +} diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/RebalanceBeforeWritingSuite.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/RebalanceBeforeWritingSuite.scala index 1d9630f4937..64e44abc08c 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/RebalanceBeforeWritingSuite.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/RebalanceBeforeWritingSuite.scala @@ -199,9 +199,10 @@ class RebalanceBeforeWritingSuite extends KyuubiSparkSQLExtensionTest { } withView("v") { - withTable("t", "input1", "input2") { + withTable("t", "t2", "input1", "input2") { withSQLConf(KyuubiSQLConf.INFER_REBALANCE_AND_SORT_ORDERS.key -> "true") { sql(s"CREATE TABLE t (c1 int, c2 long) USING PARQUET PARTITIONED BY (p string)") + sql(s"CREATE TABLE t2 (c1 int, c2 long, c3 long) USING PARQUET PARTITIONED BY (p string)") sql(s"CREATE TABLE input1 USING PARQUET AS SELECT * FROM VALUES(1,2),(1,3)") sql(s"CREATE TABLE input2 USING PARQUET AS SELECT * FROM VALUES(1,3),(1,3)") sql(s"CREATE VIEW v as SELECT col1, count(*) as col2 FROM input1 GROUP BY col1") @@ -264,6 +265,30 @@ class RebalanceBeforeWritingSuite extends KyuubiSparkSQLExtensionTest { |SELECT * FROM v |""".stripMargin) checkShuffleAndSort(df5.queryExecution.analyzed, 1, 1) + + // generate + val df6 = sql( + s""" + |INSERT INTO TABLE t2 PARTITION(p='a') + |SELECT /*+ broadcast(input2) */ input1.col1, input2.col1, cast(cc.action1 as bigint) + |FROM input1 + |JOIN input2 + |ON input1.col1 = input2.col1 + | lateral view explode(ARRAY(input1.col1, input1.col2)) cc as action1 + |""".stripMargin) + checkShuffleAndSort(df6.queryExecution.analyzed, 1, 1) + + // window + val df7 = sql( + s""" + |INSERT INTO TABLE t2 PARTITION(p='a') + |SELECT /*+ broadcast(input2) */ input1.col1, input2.col2, + | RANK() OVER (PARTITION BY input2.col2 ORDER BY input1.col1) AS rank + |FROM input1 + |JOIN input2 + |ON input1.col1 = input2.col1 + |""".stripMargin) + checkShuffleAndSort(df7.queryExecution.analyzed, 1, 1) } } } diff --git a/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala index a202e813c5e..139efd9ca06 100644 --- a/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala +++ b/extensions/spark/kyuubi-extension-spark-3-5/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala @@ -24,7 +24,7 @@ import scala.collection.JavaConverters._ import org.apache.commons.io.FileUtils import org.apache.spark.sql.catalyst.plans.logical.{GlobalLimit, LogicalPlan} -import org.apache.kyuubi.sql.KyuubiSQLConf +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} import org.apache.kyuubi.sql.watchdog.{MaxFileSizeExceedException, MaxPartitionExceedException} trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { @@ -598,4 +598,13 @@ trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { } } } + + test("disable script transformation") { + withSQLConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED.key -> "false") { + val e = intercept[KyuubiSQLExtensionException] { + sql("SELECT TRANSFORM('') USING 'ls /'") + } + assert(e.getMessage == "Script transformation is not allowed") + } + } } diff --git a/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala b/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala index 6f45dae126e..4b16d3e1681 100644 --- a/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala +++ b/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala @@ -273,4 +273,11 @@ object KyuubiSQLConf { .version("1.8.0") .stringConf .createOptional + + val SCRIPT_TRANSFORMATION_ENABLED = + buildConf("spark.sql.execution.scriptTransformation.enabled") + .doc("When false, script transformation is not allowed.") + .version("1.9.0") + .booleanConf + .createWithDefault(true) } diff --git a/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala b/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala new file mode 100644 index 00000000000..2b4d3940ada --- /dev/null +++ b/extensions/spark/kyuubi-extension-spark-common/src/main/scala/org/apache/kyuubi/sql/watchdog/KyuubiUnsupportedOperationsCheck.scala @@ -0,0 +1,35 @@ +/* + * 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.kyuubi.sql.watchdog + +import org.apache.spark.sql.catalyst.SQLConfHelper +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, ScriptTransformation} + +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} + +object KyuubiUnsupportedOperationsCheck extends (LogicalPlan => Unit) with SQLConfHelper { + override def apply(plan: LogicalPlan): Unit = + conf.getConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED) match { + case false => plan foreach { + case _: ScriptTransformation => + throw new KyuubiSQLExtensionException("Script transformation is not allowed") + case _ => + } + case true => + } +} diff --git a/extensions/spark/kyuubi-extension-spark-common/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala b/extensions/spark/kyuubi-extension-spark-common/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala index a202e813c5e..139efd9ca06 100644 --- a/extensions/spark/kyuubi-extension-spark-common/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala +++ b/extensions/spark/kyuubi-extension-spark-common/src/test/scala/org/apache/spark/sql/WatchDogSuiteBase.scala @@ -24,7 +24,7 @@ import scala.collection.JavaConverters._ import org.apache.commons.io.FileUtils import org.apache.spark.sql.catalyst.plans.logical.{GlobalLimit, LogicalPlan} -import org.apache.kyuubi.sql.KyuubiSQLConf +import org.apache.kyuubi.sql.{KyuubiSQLConf, KyuubiSQLExtensionException} import org.apache.kyuubi.sql.watchdog.{MaxFileSizeExceedException, MaxPartitionExceedException} trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { @@ -598,4 +598,13 @@ trait WatchDogSuiteBase extends KyuubiSparkSQLExtensionTest { } } } + + test("disable script transformation") { + withSQLConf(KyuubiSQLConf.SCRIPT_TRANSFORMATION_ENABLED.key -> "false") { + val e = intercept[KyuubiSQLExtensionException] { + sql("SELECT TRANSFORM('') USING 'ls /'") + } + assert(e.getMessage == "Script transformation is not allowed") + } + } } diff --git a/extensions/spark/kyuubi-spark-authz-shaded/pom.xml b/extensions/spark/kyuubi-spark-authz-shaded/pom.xml index b135a1d7c1e..10edeb1fbab 100644 --- a/extensions/spark/kyuubi-spark-authz-shaded/pom.xml +++ b/extensions/spark/kyuubi-spark-authz-shaded/pom.xml @@ -93,14 +93,6 @@ javax.ws.rs jsr311-api - - org.codehaus.jackson - jackson-core-asl - - - org.codehaus.jackson - jackson-mapper-asl - com.kstruct gethostname4j @@ -246,6 +238,8 @@ org.apache.ranger:ranger-plugins-common org.apache.ranger:ranger-plugins-audit org.codehaus.jackson:jackson-jaxrs + org.codehaus.jackson:jackson-core-asl + org.codehaus.jackson:jackson-mapper-asl com.sun.jersey:jersey-client com.sun.jersey:jersey-core com.kstruct:gethostname4j @@ -259,6 +253,9 @@ **/*.proto META-INF/*.SF + META-INF/LGPL2.1 + META-INF/AL2.0 + META-INF/ASL2.0 META-INF/*.DSA META-INF/*.RSA META-INF/DEPENDENCIES @@ -274,8 +271,8 @@ - org.codehaus.jackson.jaxrs - ${kyuubi.shade.packageName}.org.codehaus.jackson.jaxrs + org.codehaus.jackson + ${kyuubi.shade.packageName}.org.codehaus.jackson com.sun.jersey @@ -314,4 +311,27 @@ + + + + + scala-2.13 + + + + org.apache.maven.plugins + maven-shade-plugin + + + + net.java.dev.jna:jna + net.java.dev.jna:jna-platform + + + + + + + + diff --git a/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/LICENSE b/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..1e6d25e885e --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/LICENSE @@ -0,0 +1,225 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +------------------------------------------------------------------------------------ + +This project bundles some components that are licensed under the + +Apache License Version 2.0 +-------------------------- +org.apache.ranger:ranger-plugins-common +org.apache.ranger:ranger-plugins-audit +org.codehaus.jackson:jackson-jaxrs +org.codehaus.jackson:jackson-core-asl +org.codehaus.jackson:jackson-mapper-asl +net.java.dev.jna:jna +net.java.dev.jna:jna-platform + +Common Development and Distribution License (CDDL) 1.1 +------------------------------------------------------ +com.sun.jersey:jersey-client +com.sun.jersey:jersey-core + +MIT license +----------- +com.kstruct:gethostname4j diff --git a/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/NOTICE b/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..9afa0f86d1c --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz-shaded/src/main/resources/META-INF/NOTICE @@ -0,0 +1,12 @@ +Apache Kyuubi +Copyright 2021-2023 The Apache Software Foundation. + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +-------------------------------------------------------------------------------- + +This binary artifact contains + +Apache Ranger +Copyright 2014-2022 The Apache Software Foundation diff --git a/extensions/spark/kyuubi-spark-authz/README.md b/extensions/spark/kyuubi-spark-authz/README.md index 9657b5b7a5c..43ee45b09a8 100644 --- a/extensions/spark/kyuubi-spark-authz/README.md +++ b/extensions/spark/kyuubi-spark-authz/README.md @@ -34,6 +34,7 @@ build/mvn clean package -DskipTests -pl :kyuubi-spark-authz_2.12 -am -Dspark.ver `-Dspark.version=` - [x] master +- [x] 3.5.x - [x] 3.4.x (default) - [x] 3.3.x - [x] 3.2.x diff --git a/extensions/spark/kyuubi-spark-authz/pom.xml b/extensions/spark/kyuubi-spark-authz/pom.xml index 1b865561275..c2d9f759556 100644 --- a/extensions/spark/kyuubi-spark-authz/pom.xml +++ b/extensions/spark/kyuubi-spark-authz/pom.xml @@ -329,6 +329,12 @@ paimon-spark-${paimon.spark.binary.version} test + + + io.delta + ${delta.artifact}_${scala.binary.version} + test + diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ActionTypeExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ActionTypeExtractor index 61fa81809b1..140d113ac0d 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ActionTypeExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ActionTypeExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.CatalogExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.CatalogExtractor index ae058a66f2e..7ae3aac52f3 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.CatalogExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.CatalogExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ColumnExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ColumnExtractor index ed76c15d107..497c7867c59 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ColumnExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.ColumnExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.DatabaseExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.DatabaseExtractor index 2a269ee5067..c2b65812559 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.DatabaseExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.DatabaseExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionExtractor index 2facb004a04..745fd1bfcd6 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor index 3bb0ee6c23e..d054f346263 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor index 2406a40e196..10222257a5d 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor index dc35a8f5104..7010766f24b 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor @@ -6,7 +6,7 @@ # (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 +# 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, @@ -19,15 +19,17 @@ org.apache.kyuubi.plugin.spark.authz.serde.CatalogTableOptionTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.CatalogTableTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.DataSourceV2RelationTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.ExpressionSeqTableExtractor -org.apache.kyuubi.plugin.spark.authz.serde.HudiDataSourceV2RelationTableExtractor -org.apache.kyuubi.plugin.spark.authz.serde.HudiMergeIntoTargetTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.HudiCallProcedureInputTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.HudiCallProcedureOutputTableExtractor +org.apache.kyuubi.plugin.spark.authz.serde.HudiDataSourceV2RelationTableExtractor +org.apache.kyuubi.plugin.spark.authz.serde.HudiMergeIntoTargetTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.IdentifierTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.LogicalRelationTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.ResolvedDbObjectNameTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.ResolvedIdentifierTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.ResolvedTableTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.StringTableExtractor +org.apache.kyuubi.plugin.spark.authz.serde.SubqueryAliasTableExtractor +org.apache.kyuubi.plugin.spark.authz.serde.TableIdentifierOptionTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.TableIdentifierTableExtractor org.apache.kyuubi.plugin.spark.authz.serde.TableTableExtractor diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor index 251a317581f..caeeefa4196 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.URIExtractor b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.URIExtractor new file mode 100644 index 00000000000..460dfeb01ae --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/META-INF/services/org.apache.kyuubi.plugin.spark.authz.serde.URIExtractor @@ -0,0 +1,32 @@ +# +# 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. +# + +org.apache.kyuubi.plugin.spark.authz.serde.BaseRelationFileIndexURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.CatalogStorageFormatURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.CatalogTableURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.DataSourceV2RelationURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.IdentifierURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.PartitionLocsSeqURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.PropertiesLocationUriExtractor +org.apache.kyuubi.plugin.spark.authz.serde.PropertiesPathUriExtractor +org.apache.kyuubi.plugin.spark.authz.serde.ResolvedTableURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.StringSeqURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.StringURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.SubqueryAliasURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.TableIdentifierOptionURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.TableIdentifierURIExtractor +org.apache.kyuubi.plugin.spark.authz.serde.TableSpecURIExtractor diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/database_command_spec.json b/extensions/spark/kyuubi-spark-authz/src/main/resources/database_command_spec.json index c640ed89bce..5891fb1e548 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/database_command_spec.json +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/database_command_spec.json @@ -4,159 +4,215 @@ "fieldName" : "child", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "ALTERDATABASE" + "opType" : "ALTERDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CreateNamespace", "databaseDescs" : [ { "fieldName" : "name", "fieldExtractor" : "ResolvedDBObjectNameDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" }, { "fieldName" : "namespace", "fieldExtractor" : "StringSeqDatabaseExtractor", "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, - "isInput" : false + "isInput" : false, + "comment" : "" }, { "fieldName" : "name", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "CREATEDATABASE" + "opType" : "CREATEDATABASE", + "uriDescs" : [ { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DescribeNamespace", "databaseDescs" : [ { "fieldName" : "namespace", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "DESCDATABASE" + "opType" : "DESCDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DropNamespace", "databaseDescs" : [ { "fieldName" : "namespace", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "DROPDATABASE" + "opType" : "DROPDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.SetCatalogAndNamespace", "databaseDescs" : [ { "fieldName" : "child", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" }, { "fieldName" : "child", "fieldExtractor" : "ResolvedDBObjectNameDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" }, { "fieldName" : "namespace", "fieldExtractor" : "StringSeqOptionDatabaseExtractor", "catalogDesc" : { "fieldName" : "catalogName", - "fieldExtractor" : "StringOptionCatalogExtractor" + "fieldExtractor" : "StringOptionCatalogExtractor", + "comment" : "" }, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "SWITCHDATABASE" + "opType" : "SWITCHDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.SetNamespaceLocation", "databaseDescs" : [ { "fieldName" : "namespace", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "ALTERDATABASE_LOCATION" + "opType" : "ALTERDATABASE_LOCATION", + "uriDescs" : [ { + "fieldName" : "location", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.SetNamespaceProperties", "databaseDescs" : [ { "fieldName" : "namespace", "fieldExtractor" : "ResolvedNamespaceDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "ALTERDATABASE" + "opType" : "ALTERDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterDatabasePropertiesCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "ALTERDATABASE" + "opType" : "ALTERDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterDatabaseSetLocationCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "ALTERDATABASE_LOCATION" + "opType" : "ALTERDATABASE_LOCATION", + "uriDescs" : [ { + "fieldName" : "location", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.AnalyzeTablesCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringOptionDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "ANALYZE_TABLE" + "opType" : "ANALYZE_TABLE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateDatabaseCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "CREATEDATABASE" + "opType" : "CREATEDATABASE", + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.DescribeDatabaseCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "DESCDATABASE" + "opType" : "DESCDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.DropDatabaseCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], - "opType" : "DROPDATABASE" + "opType" : "DROPDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.SetDatabaseCommand", "databaseDescs" : [ { "fieldName" : "databaseName", "fieldExtractor" : "StringDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "SWITCHDATABASE" + "opType" : "SWITCHDATABASE", + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.SetNamespaceCommand", "databaseDescs" : [ { "fieldName" : "namespace", "fieldExtractor" : "StringSeqDatabaseExtractor", "catalogDesc" : null, - "isInput" : true + "isInput" : true, + "comment" : "" } ], - "opType" : "SWITCHDATABASE" + "opType" : "SWITCHDATABASE", + "uriDescs" : [ ] } ] \ No newline at end of file diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/function_command_spec.json b/extensions/spark/kyuubi-spark-authz/src/main/resources/function_command_spec.json index 0b71245d218..14dad8e2a3f 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/function_command_spec.json +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/function_command_spec.json @@ -7,9 +7,11 @@ "functionTypeDesc" : { "fieldName" : "isTemp", "fieldExtractor" : "TempMarkerFunctionTypeExtractor", - "skipTypes" : [ "TEMP" ] + "skipTypes" : [ "TEMP" ], + "comment" : "" }, - "isInput" : false + "isInput" : false, + "comment" : "" }, { "fieldName" : "functionName", "fieldExtractor" : "StringFunctionExtractor", @@ -17,14 +19,17 @@ "fieldName" : "databaseName", "fieldExtractor" : "StringOptionDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" }, "functionTypeDesc" : { "fieldName" : "isTemp", "fieldExtractor" : "TempMarkerFunctionTypeExtractor", - "skipTypes" : [ "TEMP" ] + "skipTypes" : [ "TEMP" ], + "comment" : "" }, - "isInput" : false + "isInput" : false, + "comment" : "" } ], "opType" : "CREATEFUNCTION" }, { @@ -36,9 +41,11 @@ "functionTypeDesc" : { "fieldName" : "info", "fieldExtractor" : "ExpressionInfoFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true + "isInput" : true, + "comment" : "" }, { "fieldName" : "functionName", "fieldExtractor" : "FunctionIdentifierFunctionExtractor", @@ -46,9 +53,11 @@ "functionTypeDesc" : { "fieldName" : "functionName", "fieldExtractor" : "FunctionIdentifierFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true + "isInput" : true, + "comment" : "" } ], "opType" : "DESCFUNCTION" }, { @@ -60,9 +69,11 @@ "functionTypeDesc" : { "fieldName" : "isTemp", "fieldExtractor" : "TempMarkerFunctionTypeExtractor", - "skipTypes" : [ "TEMP" ] + "skipTypes" : [ "TEMP" ], + "comment" : "" }, - "isInput" : false + "isInput" : false, + "comment" : "" }, { "fieldName" : "functionName", "fieldExtractor" : "StringFunctionExtractor", @@ -70,14 +81,17 @@ "fieldName" : "databaseName", "fieldExtractor" : "StringOptionDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" }, "functionTypeDesc" : { "fieldName" : "isTemp", "fieldExtractor" : "TempMarkerFunctionTypeExtractor", - "skipTypes" : [ "TEMP" ] + "skipTypes" : [ "TEMP" ], + "comment" : "" }, - "isInput" : false + "isInput" : false, + "comment" : "" } ], "opType" : "DROPFUNCTION" }, { @@ -89,10 +103,12 @@ "fieldName" : "databaseName", "fieldExtractor" : "StringOptionDatabaseExtractor", "catalogDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" }, "functionTypeDesc" : null, - "isInput" : false + "isInput" : false, + "comment" : "" } ], "opType" : "RELOADFUNCTION" } ] \ No newline at end of file diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/scan_command_spec.json b/extensions/spark/kyuubi-spark-authz/src/main/resources/scan_command_spec.json index 3273ccbeaf0..1145adbe07a 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/scan_command_spec.json +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/scan_command_spec.json @@ -1,35 +1,48 @@ [ { - "classname" : "org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker", + "classname" : "org.apache.kyuubi.plugin.spark.authz.rule.permanentview.PermanentViewMarker", "scanDescs" : [ { "fieldName" : "catalogTable", "fieldExtractor" : "CatalogTableTableExtractor", - "catalogDesc" : null + "catalogDesc" : null, + "comment" : "" } ], - "functionDescs" : [ ] + "functionDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.catalog.HiveTableRelation", "scanDescs" : [ { "fieldName" : "tableMeta", "fieldExtractor" : "CatalogTableTableExtractor", - "catalogDesc" : null + "catalogDesc" : null, + "comment" : "" } ], - "functionDescs" : [ ] + "functionDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.LogicalRelation", "scanDescs" : [ { "fieldName" : "catalogTable", "fieldExtractor" : "CatalogTableOptionTableExtractor", - "catalogDesc" : null + "catalogDesc" : null, + "comment" : "" } ], - "functionDescs" : [ ] + "functionDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "relation", + "fieldExtractor" : "BaseRelationFileIndexURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation", "scanDescs" : [ { "fieldName" : null, "fieldExtractor" : "DataSourceV2RelationTableExtractor", - "catalogDesc" : null + "catalogDesc" : null, + "comment" : "" } ], - "functionDescs" : [ ] + "functionDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hive.HiveGenericUDF", "scanDescs" : [ ], @@ -40,10 +53,13 @@ "functionTypeDesc" : { "fieldName" : "name", "fieldExtractor" : "FunctionNameFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true - } ] + "isInput" : true, + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hive.HiveGenericUDTF", "scanDescs" : [ ], @@ -54,10 +70,13 @@ "functionTypeDesc" : { "fieldName" : "name", "fieldExtractor" : "FunctionNameFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true - } ] + "isInput" : true, + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hive.HiveSimpleUDF", "scanDescs" : [ ], @@ -68,10 +87,13 @@ "functionTypeDesc" : { "fieldName" : "name", "fieldExtractor" : "FunctionNameFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true - } ] + "isInput" : true, + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hive.HiveUDAFFunction", "scanDescs" : [ ], @@ -82,8 +104,11 @@ "functionTypeDesc" : { "fieldName" : "name", "fieldExtractor" : "FunctionNameFunctionTypeExtractor", - "skipTypes" : [ "TEMP", "SYSTEM" ] + "skipTypes" : [ "TEMP", "SYSTEM" ], + "comment" : "" }, - "isInput" : true - } ] + "isInput" : true, + "comment" : "" + } ], + "uriDescs" : [ ] } ] \ No newline at end of file diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/table_command_spec.json b/extensions/spark/kyuubi-spark-authz/src/main/resources/table_command_spec.json index ea6e2757621..b555bbcf8be 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/resources/table_command_spec.json +++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/table_command_spec.json @@ -8,10 +8,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDCOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.AddPartitions", "tableDescs" : [ { @@ -22,10 +29,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.AlterColumn", "tableDescs" : [ { @@ -36,10 +45,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDCOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.AlterTable", "tableDescs" : [ { @@ -50,10 +66,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "ident", + "fieldExtractor" : "IdentifierURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.AppendData", "tableDescs" : [ { @@ -63,17 +86,26 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "INSERT" + "actionType" : "INSERT", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "DataSourceV2RelationURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CacheTable", @@ -81,30 +113,20 @@ "opType" : "CREATEVIEW", "queryDescs" : [ { "fieldName" : "table", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CacheTableAsSelect", "tableDescs" : [ ], "opType" : "CREATEVIEW", "queryDescs" : [ { "fieldName" : "plan", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] -}, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.Call", - "tableDescs" : [ { - "fieldName" : "args", - "fieldExtractor" : "ExpressionSeqTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : null, - "tableTypeDesc" : null, - "catalogDesc" : null, - "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" } ], - "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CommentOnTable", "tableDescs" : [ { @@ -115,10 +137,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CreateTable", "tableDescs" : [ { @@ -129,7 +153,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableName", "fieldExtractor" : "IdentifierTableExtractor", @@ -138,10 +163,12 @@ "tableTypeDesc" : null, "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "child", "fieldExtractor" : "ResolvedDbObjectNameTableExtractor", @@ -150,10 +177,27 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "tableSpec", + "fieldExtractor" : "TableSpecURIExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "tableName", + "fieldExtractor" : "IdentifierURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CreateTableAsSelect", "tableDescs" : [ { @@ -164,7 +208,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableName", "fieldExtractor" : "IdentifierTableExtractor", @@ -173,10 +218,12 @@ "tableTypeDesc" : null, "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "name", "fieldExtractor" : "ResolvedDbObjectNameTableExtractor", @@ -185,12 +232,25 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "tableSpec", + "fieldExtractor" : "TableSpecURIExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.CreateV2Table", @@ -202,31 +262,26 @@ "tableTypeDesc" : null, "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] -}, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.DeleteFromIcebergTable", - "tableDescs" : [ { - "fieldName" : "table", - "fieldExtractor" : "DataSourceV2RelationTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : { - "fieldName" : null, - "fieldExtractor" : null, - "actionType" : "UPDATE" - }, - "tableTypeDesc" : null, - "catalogDesc" : null, + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", "isInput" : false, - "setCurrentDatabaseIfMissing" : false - } ], - "opType" : "QUERY", - "queryDescs" : [ ] + "comment" : "" + }, { + "fieldName" : "tableName", + "fieldExtractor" : "IdentifierURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DeleteFromTable", "tableDescs" : [ { @@ -236,15 +291,18 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DescribeRelation", "tableDescs" : [ { @@ -255,10 +313,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "DESCTABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DropColumns", "tableDescs" : [ { @@ -269,10 +329,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDCOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DropPartitions", "tableDescs" : [ { @@ -283,10 +350,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_DROPPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.DropTable", "tableDescs" : [ { @@ -297,7 +366,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "child", "fieldExtractor" : "ResolvedTableTableExtractor", @@ -306,31 +376,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "DROPTABLE", - "queryDescs" : [ ] -}, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.MergeIntoIcebergTable", - "tableDescs" : [ { - "fieldName" : "targetTable", - "fieldExtractor" : "DataSourceV2RelationTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : { - "fieldName" : null, - "fieldExtractor" : null, - "actionType" : "UPDATE" - }, - "tableTypeDesc" : null, - "catalogDesc" : null, - "isInput" : false, - "setCurrentDatabaseIfMissing" : false - } ], - "opType" : "QUERY", - "queryDescs" : [ { - "fieldName" : "sourceTable", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.MergeIntoTable", "tableDescs" : [ { @@ -340,18 +391,22 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "sourceTable", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.OverwriteByExpression", "tableDescs" : [ { @@ -361,17 +416,26 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "INSERT_OVERWRITE" + "actionType" : "INSERT_OVERWRITE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "DataSourceV2RelationURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.OverwritePartitionsDynamic", @@ -382,17 +446,26 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "INSERT_OVERWRITE" + "actionType" : "INSERT_OVERWRITE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "DataSourceV2RelationURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.RefreshTable", @@ -404,10 +477,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.RenameColumn", "tableDescs" : [ { @@ -418,10 +493,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_RENAMECOL", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.RenamePartitions", "tableDescs" : [ { @@ -432,10 +514,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_RENAMEPART", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.RepairTable", "tableDescs" : [ { @@ -446,10 +530,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "MSCK", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ReplaceColumns", "tableDescs" : [ { @@ -460,10 +546,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_REPLACECOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ReplaceData", "tableDescs" : [ { @@ -473,18 +566,22 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ReplaceTable", "tableDescs" : [ { @@ -495,7 +592,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableName", "fieldExtractor" : "IdentifierTableExtractor", @@ -504,10 +602,12 @@ "tableTypeDesc" : null, "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "child", "fieldExtractor" : "ResolvedDbObjectNameTableExtractor", @@ -516,10 +616,27 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "tableSpec", + "fieldExtractor" : "TableSpecURIExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "tableName", + "fieldExtractor" : "IdentifierURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ReplaceTableAsSelect", "tableDescs" : [ { @@ -530,7 +647,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableName", "fieldExtractor" : "IdentifierTableExtractor", @@ -539,10 +657,12 @@ "tableTypeDesc" : null, "catalogDesc" : { "fieldName" : "catalog", - "fieldExtractor" : "CatalogPluginCatalogExtractor" + "fieldExtractor" : "CatalogPluginCatalogExtractor", + "comment" : "" }, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "name", "fieldExtractor" : "ResolvedDbObjectNameTableExtractor", @@ -551,12 +671,46 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "tableSpec", + "fieldExtractor" : "TableSpecURIExtractor", + "isInput" : false, + "comment" : "" + }, { + "fieldName" : "properties", + "fieldExtractor" : "PropertiesLocationUriExtractor", + "isInput" : false, + "comment" : "" + } ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.SetTableProperties", + "tableDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "ResolvedTableTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "" + } ], + "opType" : "ALTERTABLE_PROPERTIES", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ShowCreateTable", @@ -568,10 +722,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOW_CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.ShowTableProperties", "tableDescs" : [ { @@ -582,10 +738,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOW_TBLPROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.TruncatePartition", "tableDescs" : [ { @@ -596,10 +754,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_DROPPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.catalyst.plans.logical.TruncateTable", "tableDescs" : [ { @@ -610,67 +770,88 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "TRUNCATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.UnresolvedMergeIntoIcebergTable", + "classname" : "org.apache.spark.sql.catalyst.plans.logical.UpdateTable", "tableDescs" : [ { - "fieldName" : "targetTable", + "fieldName" : "table", "fieldExtractor" : "DataSourceV2RelationTableExtractor", "columnDesc" : null, "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ { - "fieldName" : "sourceTable", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.execution.command.AddArchivesCommand", + "tableDescs" : [ ], + "opType" : "ADD", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "paths", + "fieldExtractor" : "StringSeqURIExtractor", + "isInput" : true, + "comment" : "" } ] }, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.UpdateIcebergTable", - "tableDescs" : [ { - "fieldName" : "table", - "fieldExtractor" : "DataSourceV2RelationTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : { - "fieldName" : null, - "fieldExtractor" : null, - "actionType" : "UPDATE" - }, - "tableTypeDesc" : null, - "catalogDesc" : null, - "isInput" : false, - "setCurrentDatabaseIfMissing" : false - } ], - "opType" : "QUERY", - "queryDescs" : [ ] + "classname" : "org.apache.spark.sql.execution.command.AddFileCommand", + "tableDescs" : [ ], + "opType" : "ADD", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : true, + "comment" : "" + } ] }, { - "classname" : "org.apache.spark.sql.catalyst.plans.logical.UpdateTable", - "tableDescs" : [ { - "fieldName" : "table", - "fieldExtractor" : "DataSourceV2RelationTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : { - "fieldName" : null, - "fieldExtractor" : null, - "actionType" : "UPDATE" - }, - "tableTypeDesc" : null, - "catalogDesc" : null, - "isInput" : false, - "setCurrentDatabaseIfMissing" : false - } ], - "opType" : "QUERY", - "queryDescs" : [ ] + "classname" : "org.apache.spark.sql.execution.command.AddFilesCommand", + "tableDescs" : [ ], + "opType" : "ADD", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "paths", + "fieldExtractor" : "StringSeqURIExtractor", + "isInput" : true, + "comment" : "" + } ] +}, { + "classname" : "org.apache.spark.sql.execution.command.AddJarCommand", + "tableDescs" : [ ], + "opType" : "ADD", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : true, + "comment" : "" + } ] +}, { + "classname" : "org.apache.spark.sql.execution.command.AddJarsCommand", + "tableDescs" : [ ], + "opType" : "ADD", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "paths", + "fieldExtractor" : "StringSeqURIExtractor", + "isInput" : true, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableAddColumnsCommand", "tableDescs" : [ { @@ -678,16 +859,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "colsToAdd", - "fieldExtractor" : "StructFieldSeqColumnExtractor" + "fieldExtractor" : "StructFieldSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDCOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableAddPartitionCommand", "tableDescs" : [ { @@ -695,16 +879,24 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpecsAndLocs", - "fieldExtractor" : "PartitionLocsSeqColumnExtractor" + "fieldExtractor" : "PartitionLocsSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_ADDPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "partitionSpecsAndLocs", + "fieldExtractor" : "PartitionLocsSeqURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableChangeColumnCommand", "tableDescs" : [ { @@ -712,16 +904,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "columnName", - "fieldExtractor" : "StringColumnExtractor" + "fieldExtractor" : "StringColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_REPLACECOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableDropPartitionCommand", "tableDescs" : [ { @@ -729,16 +924,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "specs", - "fieldExtractor" : "PartitionSeqColumnExtractor" + "fieldExtractor" : "PartitionSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_DROPPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableRecoverPartitionsCommand", "tableDescs" : [ { @@ -749,10 +947,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "MSCK", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableRenameCommand", "tableDescs" : [ { @@ -763,14 +963,17 @@ "tableTypeDesc" : { "fieldName" : "oldName", "fieldExtractor" : "TableIdentifierTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW" ], + "comment" : "" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_RENAME", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableRenamePartitionCommand", "tableDescs" : [ { @@ -778,16 +981,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "oldPartition", - "fieldExtractor" : "PartitionColumnExtractor" + "fieldExtractor" : "PartitionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_RENAMEPART", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableSerDePropertiesCommand", "tableDescs" : [ { @@ -795,16 +1001,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partSpec", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_SERDEPROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableSetLocationCommand", "tableDescs" : [ { @@ -812,16 +1021,24 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpec", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_LOCATION", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "location", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableSetPropertiesCommand", "tableDescs" : [ { @@ -832,10 +1049,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterTableUnsetPropertiesCommand", "tableDescs" : [ { @@ -846,10 +1065,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AlterViewAsCommand", "tableDescs" : [ { @@ -860,17 +1081,21 @@ "tableTypeDesc" : { "fieldName" : "name", "fieldExtractor" : "TableIdentifierTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW" ], + "comment" : "" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERVIEW_AS", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AnalyzeColumnCommand", "tableDescs" : [ { @@ -881,34 +1106,40 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableIdent", "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "columnNames", - "fieldExtractor" : "StringSeqColumnExtractor" + "fieldExtractor" : "StringSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableIdent", "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "columnNames", - "fieldExtractor" : "StringSeqOptionColumnExtractor" + "fieldExtractor" : "StringSeqOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AnalyzePartitionCommand", "tableDescs" : [ { @@ -919,22 +1150,26 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableIdent", "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpec", - "fieldExtractor" : "PartitionColumnExtractor" + "fieldExtractor" : "PartitionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.AnalyzeTableCommand", "tableDescs" : [ { @@ -945,7 +1180,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" }, { "fieldName" : "tableIdent", "fieldExtractor" : "TableIdentifierTableExtractor", @@ -954,18 +1190,22 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.CacheTableCommand", "tableDescs" : [ ], "opType" : "CREATEVIEW", "queryDescs" : [ { "fieldName" : "plan", - "fieldExtractor" : "LogicalPlanOptionQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanOptionQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateDataSourceTableAsSelectCommand", "tableDescs" : [ { @@ -976,12 +1216,20 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateDataSourceTableCommand", @@ -993,10 +1241,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateTableCommand", "tableDescs" : [ { @@ -1007,10 +1262,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateTableLikeCommand", "tableDescs" : [ { @@ -1021,7 +1283,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" }, { "fieldName" : "sourceTable", "fieldExtractor" : "TableIdentifierTableExtractor", @@ -1030,10 +1293,17 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "fileFormat", + "fieldExtractor" : "CatalogStorageFormatURIExtractor", + "isInput" : false, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.CreateViewCommand", "tableDescs" : [ { @@ -1044,20 +1314,25 @@ "tableTypeDesc" : { "fieldName" : "viewType", "fieldExtractor" : "ViewTypeTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW", "GLOBAL_TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW", "GLOBAL_TEMP_VIEW" ], + "comment" : "" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATEVIEW", "queryDescs" : [ { "fieldName" : "plan", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" }, { "fieldName" : "child", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.DescribeColumnCommand", "tableDescs" : [ { @@ -1065,16 +1340,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "colNameParts", - "fieldExtractor" : "StringSeqLastColumnExtractor" + "fieldExtractor" : "StringSeqLastColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "DESCTABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.DescribeTableCommand", "tableDescs" : [ { @@ -1082,16 +1360,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpec", - "fieldExtractor" : "PartitionColumnExtractor" + "fieldExtractor" : "PartitionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "DESCTABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.DropTableCommand", "tableDescs" : [ { @@ -1102,21 +1383,31 @@ "tableTypeDesc" : { "fieldName" : "tableName", "fieldExtractor" : "TableIdentifierTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW" ], + "comment" : "" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "DROPTABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.InsertIntoDataSourceDirCommand", "tableDescs" : [ ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "storage", + "fieldExtractor" : "CatalogStorageFormatURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.execution.command.LoadDataCommand", @@ -1125,20 +1416,29 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partition", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : { "fieldName" : "isOverwrite", "fieldExtractor" : "OverwriteOrInsertActionTypeExtractor", - "actionType" : null + "actionType" : null, + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "LOAD", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : true, + "comment" : "" + } ] }, { "classname" : "org.apache.spark.sql.execution.command.RefreshTableCommand", "tableDescs" : [ { @@ -1149,10 +1449,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.RepairTableCommand", "tableDescs" : [ { @@ -1163,10 +1465,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "MSCK", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.ShowColumnsCommand", "tableDescs" : [ { @@ -1177,10 +1481,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOWCOLUMNS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.ShowCreateTableAsSerdeCommand", "tableDescs" : [ { @@ -1191,10 +1497,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOW_CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.ShowCreateTableCommand", "tableDescs" : [ { @@ -1205,10 +1513,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOW_CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.ShowPartitionsCommand", "tableDescs" : [ { @@ -1216,16 +1526,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "spec", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOWPARTITIONS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.ShowTablePropertiesCommand", "tableDescs" : [ { @@ -1236,10 +1549,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "SHOW_TBLPROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.command.TruncateTableCommand", "tableDescs" : [ { @@ -1247,16 +1562,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpec", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "TRUNCATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.CreateTable", "tableDescs" : [ { @@ -1267,18 +1585,27 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanOptionQueryExtractor" + "fieldExtractor" : "LogicalPlanOptionQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "tableDesc", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.execution.datasources.CreateTempViewUsing", "tableDescs" : [ ], "opType" : "CREATEVIEW", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.InsertIntoDataSourceCommand", "tableDescs" : [ { @@ -1288,18 +1615,22 @@ "actionTypeDesc" : { "fieldName" : "overwrite", "fieldExtractor" : "OverwriteOrInsertActionTypeExtractor", - "actionType" : null + "actionType" : null, + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.InsertIntoHadoopFsRelationCommand", "tableDescs" : [ { @@ -1307,23 +1638,28 @@ "fieldExtractor" : "CatalogTableOptionTableExtractor", "columnDesc" : { "fieldName" : "outputColumnNames", - "fieldExtractor" : "StringSeqColumnExtractor" + "fieldExtractor" : "StringSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : { "fieldName" : "mode", "fieldExtractor" : "SaveModeActionTypeExtractor", - "actionType" : null + "actionType" : null, + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.RefreshTable", "tableDescs" : [ { @@ -1334,17 +1670,26 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.execution.datasources.SaveIntoDataSourceCommand", "tableDescs" : [ ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "options", + "fieldExtractor" : "PropertiesPathUriExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.hive.execution.CreateHiveTableAsSelectCommand", @@ -1353,18 +1698,27 @@ "fieldExtractor" : "CatalogTableTableExtractor", "columnDesc" : { "fieldName" : "outputColumnNames", - "fieldExtractor" : "StringSeqColumnExtractor" + "fieldExtractor" : "StringSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "tableDesc", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.hive.execution.InsertIntoHiveDirCommand", @@ -1372,7 +1726,14 @@ "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "storage", + "fieldExtractor" : "CatalogStorageFormatURIExtractor", + "isInput" : false, + "comment" : "" } ] }, { "classname" : "org.apache.spark.sql.hive.execution.InsertIntoHiveTable", @@ -1381,23 +1742,28 @@ "fieldExtractor" : "CatalogTableTableExtractor", "columnDesc" : { "fieldName" : "outputColumnNames", - "fieldExtractor" : "StringSeqColumnExtractor" + "fieldExtractor" : "StringSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : { "fieldName" : "overwrite", "fieldExtractor" : "OverwriteOrInsertActionTypeExtractor", - "actionType" : null + "actionType" : null, + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hive.execution.OptimizedCreateHiveTableAsSelectCommand", "tableDescs" : [ { @@ -1405,19 +1771,136 @@ "fieldExtractor" : "CatalogTableTableExtractor", "columnDesc" : { "fieldName" : "outputColumnNames", - "fieldExtractor" : "StringSeqColumnExtractor" + "fieldExtractor" : "StringSeqColumnExtractor", + "comment" : "" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ { + "fieldName" : "tableDesc", + "fieldExtractor" : "CatalogTableURIExtractor", + "isInput" : false, + "comment" : "" } ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.Call", + "tableDescs" : [ { + "fieldName" : "args", + "fieldExtractor" : "ExpressionSeqTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Iceberg" + } ], + "opType" : "ALTERTABLE_PROPERTIES", + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.DeleteFromIcebergTable", + "tableDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "DataSourceV2RelationTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Iceberg" + } ], + "opType" : "QUERY", + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.MergeIntoIcebergTable", + "tableDescs" : [ { + "fieldName" : "targetTable", + "fieldExtractor" : "DataSourceV2RelationTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Iceberg" + } ], + "opType" : "QUERY", + "queryDescs" : [ { + "fieldName" : "sourceTable", + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.UnresolvedMergeIntoIcebergTable", + "tableDescs" : [ { + "fieldName" : "targetTable", + "fieldExtractor" : "DataSourceV2RelationTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Iceberg" + } ], + "opType" : "QUERY", + "queryDescs" : [ { + "fieldName" : "sourceTable", + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.catalyst.plans.logical.UpdateIcebergTable", + "tableDescs" : [ { + "fieldName" : "table", + "fieldExtractor" : "DataSourceV2RelationTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Iceberg" + } ], + "opType" : "QUERY", + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.AlterHoodieTableAddColumnsCommand", "tableDescs" : [ { @@ -1425,16 +1908,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "colsToAdd", - "fieldExtractor" : "StructFieldSeqColumnExtractor" + "fieldExtractor" : "StructFieldSeqColumnExtractor", + "comment" : "Hudi" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_ADDCOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.AlterHoodieTableChangeColumnCommand", "tableDescs" : [ { @@ -1442,16 +1928,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "columnName", - "fieldExtractor" : "StringColumnExtractor" + "fieldExtractor" : "StringColumnExtractor", + "comment" : "Hudi" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_REPLACECOLS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.AlterHoodieTableDropPartitionCommand", "tableDescs" : [ { @@ -1459,16 +1948,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpecs", - "fieldExtractor" : "PartitionSeqColumnExtractor" + "fieldExtractor" : "PartitionSeqColumnExtractor", + "comment" : "Hudi" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_DROPPARTS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.AlterHoodieTableRenameCommand", "tableDescs" : [ { @@ -1479,14 +1971,17 @@ "tableTypeDesc" : { "fieldName" : "oldName", "fieldExtractor" : "TableIdentifierTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW" ], + "comment" : "Hudi" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_RENAME", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.AlterTableCommand", "tableDescs" : [ { @@ -1497,10 +1992,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.CallProcedureHoodieCommand", "tableDescs" : [ { @@ -1510,12 +2007,14 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "OTHER" + "actionType" : "OTHER", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" }, { "fieldName" : "clone", "fieldExtractor" : "HudiCallProcedureOutputTableExtractor", @@ -1523,15 +2022,29 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.hudi.command.CompactionHoodiePathCommand", + "tableDescs" : [ ], + "opType" : "CREATETABLE", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "Hudi" + } ] }, { "classname" : "org.apache.spark.sql.hudi.command.CompactionHoodieTableCommand", "tableDescs" : [ { @@ -1542,19 +2055,23 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false - }, { - "fieldName" : "table", - "fieldExtractor" : "CatalogTableTableExtractor", - "columnDesc" : null, - "actionTypeDesc" : null, - "tableTypeDesc" : null, - "catalogDesc" : null, - "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "org.apache.spark.sql.hudi.command.CompactionShowHoodiePathCommand", + "tableDescs" : [ ], + "opType" : "SHOW_TBLPROPERTIES", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : true, + "comment" : "Hudi" + } ] }, { "classname" : "org.apache.spark.sql.hudi.command.CompactionShowHoodieTableCommand", "tableDescs" : [ { @@ -1565,10 +2082,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "SHOW_TBLPROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.CreateHoodieTableAsSelectCommand", "tableDescs" : [ { @@ -1579,13 +2098,16 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "CREATETABLE_AS_SELECT", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.CreateHoodieTableCommand", "tableDescs" : [ { @@ -1596,10 +2118,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.CreateHoodieTableLikeCommand", "tableDescs" : [ { @@ -1610,7 +2134,8 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "Hudi" }, { "fieldName" : "sourceTable", "fieldExtractor" : "TableIdentifierTableExtractor", @@ -1619,10 +2144,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : true + "setCurrentDatabaseIfMissing" : true, + "comment" : "Hudi" } ], "opType" : "CREATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.CreateIndexCommand", "tableDescs" : [ { @@ -1633,10 +2160,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "CREATEINDEX", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.DeleteHoodieTableCommand", "tableDescs" : [ { @@ -1646,15 +2175,18 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "Hudi" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.DropHoodieTableCommand", "tableDescs" : [ { @@ -1665,14 +2197,17 @@ "tableTypeDesc" : { "fieldName" : "tableIdentifier", "fieldExtractor" : "TableIdentifierTableTypeExtractor", - "skipTypes" : [ "TEMP_VIEW" ] + "skipTypes" : [ "TEMP_VIEW" ], + "comment" : "Hudi" }, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "DROPTABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.DropIndexCommand", "tableDescs" : [ { @@ -1683,10 +2218,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "DROPINDEX", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.InsertIntoHoodieTableCommand", "tableDescs" : [ { @@ -1696,18 +2233,22 @@ "actionTypeDesc" : { "fieldName" : "overwrite", "fieldExtractor" : "OverwriteOrInsertActionTypeExtractor", - "actionType" : null + "actionType" : null, + "comment" : "Hudi" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "query", - "fieldExtractor" : "LogicalPlanQueryExtractor" - } ] + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.MergeIntoHoodieTableCommand", "tableDescs" : [ { @@ -1717,18 +2258,22 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "Hudi" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "QUERY", "queryDescs" : [ { "fieldName" : "mergeInto", - "fieldExtractor" : "HudiMergeIntoSourceTableExtractor" - } ] + "fieldExtractor" : "HudiMergeIntoSourceTableExtractor", + "comment" : "Hudi" + } ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.RefreshIndexCommand", "tableDescs" : [ { @@ -1739,10 +2284,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERINDEX_REBUILD", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.RepairHoodieTableCommand", "tableDescs" : [ { @@ -1753,10 +2300,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "" } ], "opType" : "MSCK", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.ShowHoodieTablePartitionsCommand", "tableDescs" : [ { @@ -1764,16 +2313,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "specOpt", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "Hudi" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "SHOWPARTITIONS", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.ShowIndexesCommand", "tableDescs" : [ { @@ -1784,10 +2336,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : true, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "SHOWINDEXES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.Spark31AlterTableCommand", "tableDescs" : [ { @@ -1798,10 +2352,12 @@ "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "ALTERTABLE_PROPERTIES", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.TruncateHoodieTableCommand", "tableDescs" : [ { @@ -1809,16 +2365,19 @@ "fieldExtractor" : "TableIdentifierTableExtractor", "columnDesc" : { "fieldName" : "partitionSpec", - "fieldExtractor" : "PartitionOptionColumnExtractor" + "fieldExtractor" : "PartitionOptionColumnExtractor", + "comment" : "Hudi" }, "actionTypeDesc" : null, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" } ], "opType" : "TRUNCATETABLE", - "queryDescs" : [ ] + "queryDescs" : [ ], + "uriDescs" : [ ] }, { "classname" : "org.apache.spark.sql.hudi.command.UpdateHoodieTableCommand", "tableDescs" : [ { @@ -1828,13 +2387,180 @@ "actionTypeDesc" : { "fieldName" : null, "fieldExtractor" : null, - "actionType" : "UPDATE" + "actionType" : "UPDATE", + "comment" : "Hudi" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Hudi" + } ], + "opType" : "QUERY", + "queryDescs" : [ ], + "uriDescs" : [ ] +}, { + "classname" : "io.delta.tables.execution.VacuumTableCommand", + "tableDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + }, { + "fieldName" : "table", + "fieldExtractor" : "TableIdentifierOptionTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + } ], + "opType" : "MSCK", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "Delta" + }, { + "fieldName" : "table", + "fieldExtractor" : "TableIdentifierOptionURIExtractor", + "isInput" : false, + "comment" : "Delta" + }, { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "Delta" + } ] +}, { + "classname" : "org.apache.spark.sql.delta.commands.DeleteCommand", + "tableDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "Delta" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + } ], + "opType" : "QUERY", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasURIExtractor", + "isInput" : false, + "comment" : "Delta" + } ] +}, { + "classname" : "org.apache.spark.sql.delta.commands.MergeIntoCommand", + "tableDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "Delta" }, "tableTypeDesc" : null, "catalogDesc" : null, "isInput" : false, - "setCurrentDatabaseIfMissing" : false + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" } ], "opType" : "QUERY", - "queryDescs" : [ ] + "queryDescs" : [ { + "fieldName" : "source", + "fieldExtractor" : "LogicalPlanQueryExtractor", + "comment" : "Delta" + } ], + "uriDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasURIExtractor", + "isInput" : false, + "comment" : "Delta" + } ] +}, { + "classname" : "org.apache.spark.sql.delta.commands.OptimizeTableCommand", + "tableDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + }, { + "fieldName" : "tableId", + "fieldExtractor" : "TableIdentifierOptionTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : null, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + } ], + "opType" : "ALTERTABLE_COMPACT", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "child", + "fieldExtractor" : "ResolvedTableURIExtractor", + "isInput" : false, + "comment" : "Delta" + }, { + "fieldName" : "tableId", + "fieldExtractor" : "TableIdentifierOptionURIExtractor", + "isInput" : false, + "comment" : "Delta" + }, { + "fieldName" : "path", + "fieldExtractor" : "StringURIExtractor", + "isInput" : false, + "comment" : "Delta" + } ] +}, { + "classname" : "org.apache.spark.sql.delta.commands.UpdateCommand", + "tableDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasTableExtractor", + "columnDesc" : null, + "actionTypeDesc" : { + "fieldName" : null, + "fieldExtractor" : null, + "actionType" : "UPDATE", + "comment" : "Delta" + }, + "tableTypeDesc" : null, + "catalogDesc" : null, + "isInput" : false, + "setCurrentDatabaseIfMissing" : false, + "comment" : "Delta" + } ], + "opType" : "QUERY", + "queryDescs" : [ ], + "uriDescs" : [ { + "fieldName" : "target", + "fieldExtractor" : "SubqueryAliasURIExtractor", + "isInput" : false, + "comment" : "Delta" + } ] } ] \ No newline at end of file diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ObjectType.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ObjectType.scala index c94bf4f8d20..c8662f29d18 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ObjectType.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ObjectType.scala @@ -23,7 +23,7 @@ object ObjectType extends Enumeration { type ObjectType = Value - val DATABASE, TABLE, VIEW, COLUMN, FUNCTION, INDEX = Value + val DATABASE, TABLE, VIEW, COLUMN, FUNCTION, INDEX, URI = Value def apply(obj: PrivilegeObject, opType: OperationType): ObjectType = { obj.privilegeObjectType match { @@ -33,6 +33,7 @@ object ObjectType extends Enumeration { case PrivilegeObjectType.TABLE_OR_VIEW if opType.toString.contains("VIEW") => VIEW case PrivilegeObjectType.TABLE_OR_VIEW => TABLE case PrivilegeObjectType.FUNCTION => FUNCTION + case PrivilegeObjectType.DFS_URI | PrivilegeObjectType.LOCAL_URI => URI } } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/OperationType.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/OperationType.scala index 3f2062b20a0..07066cc270e 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/OperationType.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/OperationType.scala @@ -22,14 +22,14 @@ object OperationType extends Enumeration { type OperationType = Value // According to https://scalameta.org/scalafmt/docs/known-issues.html // format: off - val ALTERDATABASE, ALTERDATABASE_LOCATION, ALTERTABLE_ADDCOLS, ALTERTABLE_ADDPARTS, - ALTERTABLE_RENAMECOL, ALTERTABLE_REPLACECOLS, ALTERTABLE_DROPPARTS, ALTERTABLE_RENAMEPART, - ALTERTABLE_RENAME, ALTERTABLE_PROPERTIES, ALTERTABLE_SERDEPROPERTIES, ALTERTABLE_LOCATION, - ALTERVIEW_AS, ALTERVIEW_RENAME, ANALYZE_TABLE, CREATEDATABASE, CREATETABLE, - CREATETABLE_AS_SELECT, CREATEFUNCTION, CREATEVIEW, DESCDATABASE, DESCFUNCTION, DESCTABLE, - DROPDATABASE, DROPFUNCTION, DROPTABLE, DROPVIEW, EXPLAIN, LOAD, MSCK, QUERY, RELOADFUNCTION, - SHOWCONF, SHOW_CREATETABLE, SHOWCOLUMNS, SHOWDATABASES, SHOWFUNCTIONS, SHOWPARTITIONS, - SHOWTABLES, SHOW_TBLPROPERTIES, SWITCHDATABASE, TRUNCATETABLE, + val ADD, ALTERDATABASE, ALTERDATABASE_LOCATION, ALTERTABLE_ADDCOLS, ALTERTABLE_ADDPARTS, + ALTERTABLE_COMPACT, ALTERTABLE_RENAMECOL, ALTERTABLE_REPLACECOLS, ALTERTABLE_DROPPARTS, + ALTERTABLE_RENAMEPART, ALTERTABLE_RENAME, ALTERTABLE_PROPERTIES, ALTERTABLE_SERDEPROPERTIES, + ALTERTABLE_LOCATION, ALTERVIEW_AS, ALTERVIEW_RENAME, ANALYZE_TABLE, CREATEDATABASE, + CREATETABLE, CREATETABLE_AS_SELECT, CREATEFUNCTION, CREATEVIEW, DESCDATABASE, DESCFUNCTION, + DESCTABLE, DROPDATABASE, DROPFUNCTION, DROPTABLE, DROPVIEW, EXPLAIN, LOAD, MSCK, QUERY, + RELOADFUNCTION, SHOWCONF, SHOW_CREATETABLE, SHOWCOLUMNS, SHOWDATABASES, SHOWFUNCTIONS, + SHOWPARTITIONS, SHOWTABLES, SHOW_TBLPROPERTIES, SWITCHDATABASE, TRUNCATETABLE, CREATEINDEX, DROPINDEX, ALTERINDEX_REBUILD, SHOWINDEXES = Value // format: on } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObject.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObject.scala index 195aa79892c..228aaeb11a7 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObject.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObject.scala @@ -17,11 +17,12 @@ package org.apache.kyuubi.plugin.spark.authz +import java.net.URI import javax.annotation.Nonnull import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType.PrivilegeObjectActionType import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectType._ -import org.apache.kyuubi.plugin.spark.authz.serde.{Database, Function, Table} +import org.apache.kyuubi.plugin.spark.authz.serde.{Database, Function, Table, Uri} /** * Build a Spark logical plan to different `PrivilegeObject`s @@ -86,4 +87,19 @@ object PrivilegeObject { None ) // TODO: Support catalog for function } + + def apply(uri: Uri): PrivilegeObject = { + val privilegeObjectType = Option(new URI(uri.path).getScheme) match { + case Some("file") => LOCAL_URI + case _ => DFS_URI + } + new PrivilegeObject( + privilegeObjectType, + PrivilegeObjectActionType.OTHER, + uri.path, + null, + Nil, + None, + None) + } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObjectType.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObjectType.scala index f514fcb828c..28b9588eaa2 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObjectType.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegeObjectType.scala @@ -20,5 +20,5 @@ package org.apache.kyuubi.plugin.spark.authz object PrivilegeObjectType extends Enumeration { type PrivilegeObjectType = Value - val DATABASE, TABLE_OR_VIEW, FUNCTION = Value + val DATABASE, TABLE_OR_VIEW, FUNCTION, LOCAL_URI, DFS_URI = Value } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilder.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilder.scala index a0ed5fb6a14..2d452ba9d67 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilder.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilder.scala @@ -22,13 +22,15 @@ import scala.collection.mutable.ArrayBuffer import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.expressions.{Expression, NamedExpression} import org.apache.spark.sql.catalyst.plans.logical._ +import org.apache.spark.sql.execution.command.ExplainCommand import org.slf4j.LoggerFactory import org.apache.kyuubi.plugin.spark.authz.OperationType.OperationType import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType._ +import org.apache.kyuubi.plugin.spark.authz.rule.Authorization._ +import org.apache.kyuubi.plugin.spark.authz.rule.rowfilter._ import org.apache.kyuubi.plugin.spark.authz.serde._ import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ -import org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker import org.apache.kyuubi.util.reflect.ReflectUtils._ object PrivilegesBuilder { @@ -74,6 +76,8 @@ object PrivilegesBuilder { } plan match { + case p if p.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty => + case p: Project => buildQuery(p.child, privilegeObjects, p.projectList, conditionList, spark) case j: Join => @@ -103,13 +107,15 @@ object PrivilegesBuilder { val cols = conditionList ++ aggCols buildQuery(a.child, privilegeObjects, projectionList, cols, spark) - case pvm: PermanentViewMarker => - getScanSpec(pvm).tables(pvm, spark).foreach { table => - privilegeObjects += PrivilegeObject(table, pvm.visitColNames) - } - case scan if isKnownScan(scan) && scan.resolved => - getScanSpec(scan).tables(scan, spark).foreach(mergeProjection(_, scan)) + val tables = getScanSpec(scan).tables(scan, spark) + // If the the scan is table-based, we check privileges on the table we found + // otherwise, we check privileges on the uri we found + if (tables.nonEmpty) { + tables.foreach(mergeProjection(_, scan)) + } else { + getScanSpec(scan).uris(scan).foreach(privilegeObjects += PrivilegeObject(_)) + } case u if u.nodeName == "UnresolvedRelation" => val parts = invokeAs[String](u, "tableName").split("\\.") @@ -178,6 +184,19 @@ object PrivilegesBuilder { LOG.debug(databaseDesc.error(plan, e)) } } + desc.uriDescs.foreach { ud => + try { + val uris = ud.extract(plan, spark) + if (ud.isInput) { + inputObjs ++= uris.map(PrivilegeObject(_)) + } else { + outputObjs ++= uris.map(PrivilegeObject(_)) + } + } catch { + case e: Exception => + LOG.debug(ud.error(plan, e)) + } + } desc.operationType case classname if TABLE_COMMAND_SPECS.contains(classname) => @@ -189,6 +208,19 @@ object PrivilegesBuilder { outputObjs ++= getTablePriv(td) } } + spec.uriDescs.foreach { ud => + try { + val uris = ud.extract(plan, spark) + if (ud.isInput) { + inputObjs ++= uris.map(PrivilegeObject(_)) + } else { + outputObjs ++= uris.map(PrivilegeObject(_)) + } + } catch { + case e: Exception => + LOG.debug(ud.error(plan, e)) + } + } spec.queries(plan).foreach(buildQuery(_, inputObjs, spark = spark)) spec.operationType @@ -265,6 +297,20 @@ object PrivilegesBuilder { val inputObjs = new ArrayBuffer[PrivilegeObject] val outputObjs = new ArrayBuffer[PrivilegeObject] val opType = plan match { + case ObjectFilterPlaceHolder(child) if child.nodeName == "ShowTables" => + OperationType.SHOWTABLES + case ObjectFilterPlaceHolder(child) if child.nodeName == "ShowNamespaces" => + OperationType.SHOWDATABASES + case _: FilteredShowTablesCommand => OperationType.SHOWTABLES + case _: FilteredShowFunctionsCommand => OperationType.SHOWFUNCTIONS + case _: FilteredShowColumnsCommand => OperationType.SHOWCOLUMNS + + // ExplainCommand run will execute the plan, should avoid check privilege for the plan. + case _: ExplainCommand => + setExplainCommandExecutionId(spark) + OperationType.EXPLAIN + case _ if isExplainCommandChild(spark) => + OperationType.EXPLAIN // RunnableCommand case cmd: Command => buildCommand(cmd, inputObjs, outputObjs, spark) // Queries diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessResource.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessResource.scala index 23cd87b2745..858dc1c3733 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessResource.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessResource.scala @@ -17,6 +17,9 @@ package org.apache.kyuubi.plugin.spark.authz.ranger +import java.io.File +import java.util + import scala.language.implicitConversions import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl @@ -35,6 +38,7 @@ class AccessResource private (val objectType: ObjectType, val catalog: Option[St val columnStr = getColumn if (columnStr == null) Nil else columnStr.split(",").filter(_.nonEmpty) } + def getUrl: String = getValue("url") } object AccessResource { @@ -60,6 +64,16 @@ object AccessResource { case TABLE | VIEW | INDEX => resource.setValue("database", firstLevelResource) resource.setValue("table", secondLevelResource) + case URI => + val objectList = new util.ArrayList[String] + Option(firstLevelResource) + .filter(_.nonEmpty) + .foreach { path => + val s = path.stripSuffix(File.separator) + objectList.add(s) + objectList.add(s + File.separator) + } + resource.setValue("url", objectList) } resource.setServiceDef(SparkRangerAdminPlugin.getServiceDef) owner.foreach(resource.setOwnerUser) diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessType.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessType.scala index d533d638bac..3a836df372f 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessType.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AccessType.scala @@ -25,11 +25,18 @@ object AccessType extends Enumeration { type AccessType = Value - val NONE, CREATE, ALTER, DROP, SELECT, UPDATE, USE, READ, WRITE, ALL, ADMIN, INDEX = Value + val NONE, CREATE, ALTER, DROP, SELECT, UPDATE, USE, READ, WRITE, ALL, ADMIN, INDEX, TEMPUDFADMIN = + Value def apply(obj: PrivilegeObject, opType: OperationType, isInput: Boolean): AccessType = { + if (obj.privilegeObjectType == DFS_URI || obj.privilegeObjectType == LOCAL_URI) { + // This is equivalent to ObjectType.URI + return if (isInput) READ else WRITE + } + obj.actionType match { case PrivilegeObjectActionType.OTHER => opType match { + case ADD => TEMPUDFADMIN case CREATEDATABASE if obj.privilegeObjectType == DATABASE => CREATE case CREATEFUNCTION if obj.privilegeObjectType == FUNCTION => CREATE case CREATETABLE | CREATEVIEW | CREATETABLE_AS_SELECT @@ -39,6 +46,7 @@ object AccessType extends Enumeration { ALTERDATABASE_LOCATION | ALTERTABLE_ADDCOLS | ALTERTABLE_ADDPARTS | + ALTERTABLE_COMPACT | ALTERTABLE_DROPPARTS | ALTERTABLE_LOCATION | ALTERTABLE_RENAME | diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RangerConfigProvider.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerConfigProvider.scala similarity index 88% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RangerConfigProvider.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerConfigProvider.scala index a61d94a8fc8..05d8cc64f40 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RangerConfigProvider.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerConfigProvider.scala @@ -15,12 +15,12 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.util +package org.apache.kyuubi.plugin.spark.authz.ranger import org.apache.hadoop.conf.Configuration -import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ -import org.apache.kyuubi.util.reflect.ReflectUtils._ +import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils.isRanger21orGreater +import org.apache.kyuubi.util.reflect.ReflectUtils.invokeAs trait RangerConfigProvider { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala index f8e941d9def..288719f07bf 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala @@ -19,9 +19,12 @@ package org.apache.kyuubi.plugin.spark.authz.ranger import org.apache.spark.sql.SparkSessionExtensions -import org.apache.kyuubi.plugin.spark.authz.ranger.datamasking.{RuleApplyDataMaskingStage0, RuleApplyDataMaskingStage1} -import org.apache.kyuubi.plugin.spark.authz.ranger.rowfilter.RuleApplyRowFilter -import org.apache.kyuubi.plugin.spark.authz.util.{RuleEliminateMarker, RuleEliminateViewMarker} +import org.apache.kyuubi.plugin.spark.authz.rule.{RuleEliminateMarker, RuleEliminatePermanentViewMarker, RuleEliminateTypeOf} +import org.apache.kyuubi.plugin.spark.authz.rule.config.AuthzConfigurationChecker +import org.apache.kyuubi.plugin.spark.authz.rule.datamasking.{RuleApplyDataMaskingStage0, RuleApplyDataMaskingStage1} +import org.apache.kyuubi.plugin.spark.authz.rule.expression.RuleApplyTypeOfMarker +import org.apache.kyuubi.plugin.spark.authz.rule.permanentview.RuleApplyPermanentViewMarker +import org.apache.kyuubi.plugin.spark.authz.rule.rowfilter.{FilterDataSourceV2Strategy, RuleApplyRowFilter, RuleReplaceShowObjectCommands} /** * ACL Management for Apache Spark SQL with Apache Ranger, enabling: @@ -42,14 +45,16 @@ class RangerSparkExtension extends (SparkSessionExtensions => Unit) { override def apply(v1: SparkSessionExtensions): Unit = { v1.injectCheckRule(AuthzConfigurationChecker) - v1.injectResolutionRule(_ => new RuleReplaceShowObjectCommands()) - v1.injectResolutionRule(_ => new RuleApplyPermanentViewMarker()) + v1.injectResolutionRule(_ => RuleReplaceShowObjectCommands) + v1.injectResolutionRule(_ => RuleApplyPermanentViewMarker) + v1.injectResolutionRule(_ => RuleApplyTypeOfMarker) v1.injectResolutionRule(RuleApplyRowFilter) v1.injectResolutionRule(RuleApplyDataMaskingStage0) v1.injectResolutionRule(RuleApplyDataMaskingStage1) - v1.injectOptimizerRule(_ => new RuleEliminateMarker()) - v1.injectOptimizerRule(new RuleAuthorization(_)) - v1.injectOptimizerRule(_ => new RuleEliminateViewMarker()) - v1.injectPlannerStrategy(new FilterDataSourceV2Strategy(_)) + v1.injectOptimizerRule(_ => RuleEliminateMarker) + v1.injectOptimizerRule(RuleAuthorization) + v1.injectOptimizerRule(RuleEliminatePermanentViewMarker) + v1.injectOptimizerRule(_ => RuleEliminateTypeOf) + v1.injectPlannerStrategy(FilterDataSourceV2Strategy) } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleAuthorization.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleAuthorization.scala index 3203108dfae..e25cd2a7004 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleAuthorization.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleAuthorization.scala @@ -22,36 +22,19 @@ import scala.collection.mutable.ArrayBuffer import org.apache.ranger.plugin.policyengine.RangerAccessRequest import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan -import org.apache.spark.sql.catalyst.rules.Rule -import org.apache.spark.sql.catalyst.trees.TreeNodeTag import org.apache.kyuubi.plugin.spark.authz._ import org.apache.kyuubi.plugin.spark.authz.ObjectType._ -import org.apache.kyuubi.plugin.spark.authz.ranger.RuleAuthorization._ import org.apache.kyuubi.plugin.spark.authz.ranger.SparkRangerAdminPlugin._ +import org.apache.kyuubi.plugin.spark.authz.rule.Authorization import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ -class RuleAuthorization(spark: SparkSession) extends Rule[LogicalPlan] { - override def apply(plan: LogicalPlan): LogicalPlan = { - plan match { - case plan if isAuthChecked(plan) => plan // do nothing if checked privileges already. - case p => checkPrivileges(spark, p) - } - } -} -object RuleAuthorization { - - val KYUUBI_AUTHZ_TAG = TreeNodeTag[Boolean]("__KYUUBI_AUTHZ_TAG") - - private def checkPrivileges(spark: SparkSession, plan: LogicalPlan): LogicalPlan = { +case class RuleAuthorization(spark: SparkSession) extends Authorization(spark) { + override def checkPrivileges(spark: SparkSession, plan: LogicalPlan): Unit = { val auditHandler = new SparkRangerAuditHandler val ugi = getAuthzUgi(spark.sparkContext) val (inputs, outputs, opType) = PrivilegesBuilder.build(plan, spark) val requests = new ArrayBuffer[AccessRequest]() - if (inputs.isEmpty && opType == OperationType.SHOWDATABASES) { - val resource = AccessResource(DATABASE, null, None) - requests += AccessRequest(resource, ugi, opType, AccessType.USE) - } def addAccessRequest(objects: Iterable[PrivilegeObject], isInput: Boolean): Unit = { objects.foreach { obj => @@ -93,17 +76,5 @@ object RuleAuthorization { verify(Seq(req), auditHandler) } } - markAuthChecked(plan) - } - - private def markAuthChecked(plan: LogicalPlan): LogicalPlan = { - plan.transformUp { case p => - p.setTagValue(KYUUBI_AUTHZ_TAG, true) - p - } - } - - private def isAuthChecked(plan: LogicalPlan): Boolean = { - plan.find(_.getTagValue(KYUUBI_AUTHZ_TAG).contains(true)).nonEmpty } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/SparkRangerAdminPlugin.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/SparkRangerAdminPlugin.scala index d3059ef2dd3..66f34db9106 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/SparkRangerAdminPlugin.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/SparkRangerAdminPlugin.scala @@ -26,7 +26,6 @@ import org.apache.ranger.plugin.service.RangerBasePlugin import org.slf4j.LoggerFactory import org.apache.kyuubi.plugin.spark.authz.AccessControlException -import org.apache.kyuubi.plugin.spark.authz.util.RangerConfigProvider object SparkRangerAdminPlugin extends RangerBasePlugin("spark", "sparkSql") with RangerConfigProvider { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/Authorization.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/Authorization.scala new file mode 100644 index 00000000000..d1494266e85 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/Authorization.scala @@ -0,0 +1,85 @@ +/* + * 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.kyuubi.plugin.spark.authz.rule + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, View} +import org.apache.spark.sql.catalyst.rules.Rule +import org.apache.spark.sql.catalyst.trees.TreeNodeTag +import org.apache.spark.sql.execution.SQLExecution.EXECUTION_ID_KEY + +import org.apache.kyuubi.plugin.spark.authz.rule.Authorization._ +import org.apache.kyuubi.plugin.spark.authz.util.ReservedKeys._ + +abstract class Authorization(spark: SparkSession) extends Rule[LogicalPlan] { + override def apply(plan: LogicalPlan): LogicalPlan = { + plan match { + case plan if isAuthChecked(plan) => plan // do nothing if checked privileges already. + case p => + checkPrivileges(spark, p) + markAuthChecked(p) + } + } + + def checkPrivileges(spark: SparkSession, plan: LogicalPlan): Unit +} + +object Authorization { + + val KYUUBI_AUTHZ_TAG = TreeNodeTag[Unit]("__KYUUBI_AUTHZ_TAG") + + private def markAllNodesAuthChecked(plan: LogicalPlan): LogicalPlan = { + plan.transformDown { case p => + p.setTagValue(KYUUBI_AUTHZ_TAG, ()) + p + } + } + + def markAuthChecked(plan: LogicalPlan): LogicalPlan = { + plan.setTagValue(KYUUBI_AUTHZ_TAG, ()) + plan transformDown { + // TODO: Add this line Support for spark3.1, we can remove this + // after spark 3.2 since https://issues.apache.org/jira/browse/SPARK-34269 + case view: View => + markAllNodesAuthChecked(view.child) + } + } + + protected def isAuthChecked(plan: LogicalPlan): Boolean = { + plan.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty + } + + def setExplainCommandExecutionId(sparkSession: SparkSession): Unit = { + sparkSession.sparkContext.setLocalProperty( + KYUUBI_EXPLAIN_COMMAND_EXECUTION_ID, + executionId(sparkSession)) + } + + def isExplainCommandChild(sparkSession: SparkSession): Boolean = { + if (null == executionId(sparkSession)) { + false + } else { + executionId(sparkSession).equals( + sparkSession.sparkContext.getLocalProperty(KYUUBI_EXPLAIN_COMMAND_EXECUTION_ID)) + } + } + + private def executionId(sparkSession: SparkSession): String = { + sparkSession.sparkContext.getLocalProperty(EXECUTION_ID_KEY) + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateMarker.scala similarity index 82% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateMarker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateMarker.scala index 448439b8426..a3a22a5f321 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateMarker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateMarker.scala @@ -15,16 +15,16 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.util +package org.apache.kyuubi.plugin.spark.authz.rule import org.apache.spark.sql.catalyst.expressions.SubqueryExpression import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan import org.apache.spark.sql.catalyst.rules.Rule -import org.apache.kyuubi.plugin.spark.authz.ranger.datamasking.{DataMaskingStage0Marker, DataMaskingStage1Marker} -import org.apache.kyuubi.plugin.spark.authz.ranger.rowfilter.RowFilterMarker +import org.apache.kyuubi.plugin.spark.authz.rule.datamasking.{DataMaskingStage0Marker, DataMaskingStage1Marker} +import org.apache.kyuubi.plugin.spark.authz.rule.rowfilter.RowFilterMarker -class RuleEliminateMarker extends Rule[LogicalPlan] { +object RuleEliminateMarker extends Rule[LogicalPlan] { override def apply(plan: LogicalPlan): LogicalPlan = { plan.transformUp { case p => p.transformExpressionsUp { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminatePermanentViewMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminatePermanentViewMarker.scala new file mode 100644 index 00000000000..003521c727b --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminatePermanentViewMarker.scala @@ -0,0 +1,61 @@ +/* + * 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.kyuubi.plugin.spark.authz.rule + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.catalyst.expressions.SubqueryExpression +import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan +import org.apache.spark.sql.catalyst.rules.Rule + +import org.apache.kyuubi.plugin.spark.authz.rule.permanentview.PermanentViewMarker + +/** + * Transforming up [[PermanentViewMarker]] + */ +case class RuleEliminatePermanentViewMarker(sparkSession: SparkSession) extends Rule[LogicalPlan] { + + def eliminatePVM(plan: LogicalPlan): LogicalPlan = { + plan.transformUp { + case pvm: PermanentViewMarker => + val ret = pvm.child.transformAllExpressions { + case s: SubqueryExpression => s.withNewPlan(eliminatePVM(s.plan)) + } + // For each SubqueryExpression's PVM, we should mark as resolved to + // avoid check privilege of PVM's internal Subquery. + Authorization.markAuthChecked(ret) + ret + } + } + + override def apply(plan: LogicalPlan): LogicalPlan = { + var matched = false + val eliminatedPVM = plan.transformUp { + case pvm: PermanentViewMarker => + matched = true + pvm.child.transformAllExpressions { + case s: SubqueryExpression => s.withNewPlan(eliminatePVM(s.plan)) + } + } + if (matched) { + Authorization.markAuthChecked(eliminatedPVM) + sparkSession.sessionState.optimizer.execute(eliminatedPVM) + } else { + eliminatedPVM + } + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateViewMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateTypeOf.scala similarity index 68% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateViewMarker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateTypeOf.scala index 8044f1283e5..0f3ae136c4a 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/RuleEliminateViewMarker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleEliminateTypeOf.scala @@ -15,21 +15,20 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.util +package org.apache.kyuubi.plugin.spark.authz.rule -import org.apache.spark.sql.catalyst.expressions.SubqueryExpression +import org.apache.spark.sql.catalyst.expressions.TypeOf import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan import org.apache.spark.sql.catalyst.rules.Rule -/** - * Transforming up [[org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker]] - */ -class RuleEliminateViewMarker extends Rule[LogicalPlan] { +import org.apache.kyuubi.plugin.spark.authz.rule.expression.TypeOfPlaceHolder + +object RuleEliminateTypeOf extends Rule[LogicalPlan] { override def apply(plan: LogicalPlan): LogicalPlan = { - plan.transformUp { - case pvm: PermanentViewMarker => pvm.child.transformAllExpressions { - case s: SubqueryExpression => s.withNewPlan(apply(s.plan)) - } + plan.transformUp { case p => + p.transformExpressionsUp { + case toph: TypeOfPlaceHolder => TypeOf(toph.expr) + } } } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleHelper.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleHelper.scala similarity index 97% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleHelper.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleHelper.scala index 3cfe2b9406b..c163cafe931 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleHelper.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/RuleHelper.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule import org.apache.hadoop.security.UserGroupInformation import org.apache.spark.sql.SparkSession diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationChecker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/AuthzConfigurationChecker.scala similarity index 97% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationChecker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/AuthzConfigurationChecker.scala index 56ab27d2244..3ab2c3fd640 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationChecker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/AuthzConfigurationChecker.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule.config import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage0Marker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage0Marker.scala similarity index 95% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage0Marker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage0Marker.scala index b4314938324..c1d3a75321e 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage0Marker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage0Marker.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.datamasking +package org.apache.kyuubi.plugin.spark.authz.rule.datamasking import org.apache.spark.sql.catalyst.expressions.{Attribute, ExprId} import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage1Marker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage1Marker.scala similarity index 95% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage1Marker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage1Marker.scala index aed0ac693b1..1c30879e4e6 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/DataMaskingStage1Marker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/DataMaskingStage1Marker.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.datamasking +package org.apache.kyuubi.plugin.spark.authz.rule.datamasking import org.apache.spark.sql.catalyst.expressions.Attribute import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage0.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage0.scala similarity index 95% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage0.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage0.scala index de125550ac9..27cde162113 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage0.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage0.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.datamasking +package org.apache.kyuubi.plugin.spark.authz.rule.datamasking import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.expressions.Alias @@ -24,6 +24,7 @@ import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project} import org.apache.kyuubi.plugin.spark.authz.ObjectType import org.apache.kyuubi.plugin.spark.authz.OperationType.QUERY import org.apache.kyuubi.plugin.spark.authz.ranger._ +import org.apache.kyuubi.plugin.spark.authz.rule.RuleHelper import org.apache.kyuubi.plugin.spark.authz.serde._ /** diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage1.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage1.scala similarity index 96% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage1.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage1.scala index 9589be2e97b..b0069c9a543 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/datamasking/RuleApplyDataMaskingStage1.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/datamasking/RuleApplyDataMaskingStage1.scala @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.datamasking +package org.apache.kyuubi.plugin.spark.authz.rule.datamasking import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.expressions.NamedExpression import org.apache.spark.sql.catalyst.plans.logical.{Command, LogicalPlan} -import org.apache.kyuubi.plugin.spark.authz.ranger.RuleHelper +import org.apache.kyuubi.plugin.spark.authz.rule.RuleHelper import org.apache.kyuubi.plugin.spark.authz.serde._ /** diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/RuleApplyTypeOfMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/RuleApplyTypeOfMarker.scala new file mode 100644 index 00000000000..8d47c56f7af --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/RuleApplyTypeOfMarker.scala @@ -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. + */ + +package org.apache.kyuubi.plugin.spark.authz.rule.expression + +import org.apache.spark.sql.catalyst.expressions.TypeOf +import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan +import org.apache.spark.sql.catalyst.rules.Rule + +object RuleApplyTypeOfMarker extends Rule[LogicalPlan] { + + override def apply(plan: LogicalPlan): LogicalPlan = { + plan transformAllExpressions { + case typeof: TypeOf => TypeOfPlaceHolder(typeof.child) + } + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/TypeOfPlaceHolder.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/TypeOfPlaceHolder.scala new file mode 100644 index 00000000000..ebc9cecf5d5 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/expression/TypeOfPlaceHolder.scala @@ -0,0 +1,41 @@ +/* + * 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.kyuubi.plugin.spark.authz.rule.expression + +import org.apache.spark.sql.catalyst.expressions.{Expression, UnaryExpression} +import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode} +import org.apache.spark.sql.types.{DataType, StringType} + +import org.apache.kyuubi.plugin.spark.authz.util.WithInternalExpressionChild + +case class TypeOfPlaceHolder(expr: Expression) extends UnaryExpression + with WithInternalExpressionChild { + override def dataType: DataType = StringType + + // Avoid fold constant expression by Spark Optimizer + override def foldable: Boolean = false + + override def child: Expression = expr + + override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { + defineCodeGen(ctx, ev, _ => s"""UTF8String.fromString(${child.dataType.catalogString})""") + } + + override def withNewChildInternal(newChild: Expression): Expression = + copy(expr = newChild) +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala new file mode 100644 index 00000000000..fc52adc0458 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/PermanentViewMarker.scala @@ -0,0 +1,43 @@ +/* + * 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.kyuubi.plugin.spark.authz.rule.permanentview + +import org.apache.spark.sql.catalyst.analysis.MultiInstanceRelation +import org.apache.spark.sql.catalyst.catalog.CatalogTable +import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, Cast} +import org.apache.spark.sql.catalyst.plans.QueryPlan +import org.apache.spark.sql.catalyst.plans.logical.{LeafNode, LogicalPlan, Project, Statistics} + +case class PermanentViewMarker(child: LogicalPlan, catalogTable: CatalogTable) + extends LeafNode with MultiInstanceRelation { + + override def output: Seq[Attribute] = child.output + + override def argString(maxFields: Int): String = "" + + override def innerChildren: Seq[QueryPlan[_]] = child :: Nil + + override def computeStats(): Statistics = child.stats + + override def newInstance(): LogicalPlan = { + val projectList = child.output.map { case attr => + Alias(Cast(attr, attr.dataType), attr.name)(explicitMetadata = Some(attr.metadata)) + } + this.copy(child = Project(projectList, child), catalogTable = catalogTable) + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleApplyPermanentViewMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/RuleApplyPermanentViewMarker.scala similarity index 62% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleApplyPermanentViewMarker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/RuleApplyPermanentViewMarker.scala index 909cd9e93d3..a84ecec8c31 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleApplyPermanentViewMarker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/permanentview/RuleApplyPermanentViewMarker.scala @@ -15,40 +15,42 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule.permanentview +import org.apache.spark.sql.catalyst.catalog.CatalogTable import org.apache.spark.sql.catalyst.expressions.SubqueryExpression import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, View} import org.apache.spark.sql.catalyst.rules.Rule import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ -import org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker /** - * Adding [[org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker]] for permanent views + * Adding [[PermanentViewMarker]] for permanent views * for marking catalogTable of views used by privilege checking * in [[org.apache.kyuubi.plugin.spark.authz.ranger.RuleAuthorization]]. - * [[org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker]] must be transformed up later - * in [[org.apache.kyuubi.plugin.spark.authz.util.RuleEliminateViewMarker]] optimizer. + * [[PermanentViewMarker]] must be transformed up later + * in [[org.apache.kyuubi.plugin.spark.authz.rule.RuleEliminatePermanentViewMarker]] optimizer. */ -class RuleApplyPermanentViewMarker extends Rule[LogicalPlan] { +object RuleApplyPermanentViewMarker extends Rule[LogicalPlan] { + + private def resolveSubqueryExpression( + plan: LogicalPlan, + catalogTable: CatalogTable): LogicalPlan = { + plan.transformAllExpressions { + case subquery: SubqueryExpression => + subquery.withNewPlan(plan = PermanentViewMarker( + resolveSubqueryExpression(subquery.plan, catalogTable), + catalogTable)) + } + } override def apply(plan: LogicalPlan): LogicalPlan = { plan mapChildren { case p: PermanentViewMarker => p case permanentView: View if hasResolvedPermanentView(permanentView) => - val resolvedSubquery = permanentView.transformAllExpressions { - case subquery: SubqueryExpression => - subquery.withNewPlan(plan = - PermanentViewMarker( - subquery.plan, - permanentView.desc, - permanentView.output.map(_.name))) - } PermanentViewMarker( - resolvedSubquery, - resolvedSubquery.desc, - resolvedSubquery.output.map(_.name)) + resolveSubqueryExpression(permanentView, permanentView.desc), + permanentView.desc) case other => apply(other) } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilterDataSourceV2Strategy.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilterDataSourceV2Strategy.scala similarity index 90% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilterDataSourceV2Strategy.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilterDataSourceV2Strategy.scala index cbf79581ed6..e268ed6bc7c 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilterDataSourceV2Strategy.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilterDataSourceV2Strategy.scala @@ -14,15 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.spark.sql.{SparkSession, Strategy} import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project} import org.apache.spark.sql.execution.SparkPlan -import org.apache.kyuubi.plugin.spark.authz.util.ObjectFilterPlaceHolder - -class FilterDataSourceV2Strategy(spark: SparkSession) extends Strategy { +case class FilterDataSourceV2Strategy(spark: SparkSession) extends Strategy { override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match { // For Spark 3.1 and below, `ColumnPruning` rule will set `ObjectFilterPlaceHolder#child` to // `Project` diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilteredShowObjectsExec.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilteredShowObjectsExec.scala similarity index 94% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilteredShowObjectsExec.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilteredShowObjectsExec.scala index 67519118ecc..0bb4213561c 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/FilteredShowObjectsExec.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/FilteredShowObjectsExec.scala @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.hadoop.security.UserGroupInformation import org.apache.spark.SparkContext @@ -24,6 +24,7 @@ import org.apache.spark.sql.catalyst.expressions.Attribute import org.apache.spark.sql.execution.{LeafExecNode, SparkPlan} import org.apache.kyuubi.plugin.spark.authz.{ObjectType, OperationType} +import org.apache.kyuubi.plugin.spark.authz.ranger.{AccessRequest, AccessResource, AccessType, SparkRangerAdminPlugin} import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils trait FilteredShowObjectsExec extends LeafExecNode { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ObjectFilterPlaceHolder.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/ObjectFilterPlaceHolder.scala similarity index 91% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ObjectFilterPlaceHolder.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/ObjectFilterPlaceHolder.scala index 0d3c39adb69..6a7f1beab18 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ObjectFilterPlaceHolder.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/ObjectFilterPlaceHolder.scala @@ -15,11 +15,13 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.util +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.spark.sql.catalyst.expressions.Attribute import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode} +import org.apache.kyuubi.plugin.spark.authz.util.WithInternalChild + case class ObjectFilterPlaceHolder(child: LogicalPlan) extends UnaryNode with WithInternalChild { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RowFilterMarker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RowFilterMarker.scala similarity index 95% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RowFilterMarker.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RowFilterMarker.scala index 8817958b585..f4295a0942f 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RowFilterMarker.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RowFilterMarker.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.rowfilter +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.spark.sql.catalyst.expressions.Attribute import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RuleApplyRowFilter.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleApplyRowFilter.scala similarity index 94% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RuleApplyRowFilter.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleApplyRowFilter.scala index 22bcfae49d9..defee4005b6 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/rowfilter/RuleApplyRowFilter.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleApplyRowFilter.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger.rowfilter +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan} @@ -23,6 +23,7 @@ import org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan} import org.apache.kyuubi.plugin.spark.authz.ObjectType import org.apache.kyuubi.plugin.spark.authz.OperationType.QUERY import org.apache.kyuubi.plugin.spark.authz.ranger._ +import org.apache.kyuubi.plugin.spark.authz.rule.RuleHelper import org.apache.kyuubi.plugin.spark.authz.serde._ case class RuleApplyRowFilter(spark: SparkSession) extends RuleHelper { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleReplaceShowObjectCommands.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleReplaceShowObjectCommands.scala similarity index 93% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleReplaceShowObjectCommands.scala rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleReplaceShowObjectCommands.scala index bf762109cba..06982d70106 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RuleReplaceShowObjectCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/rowfilter/RuleReplaceShowObjectCommands.scala @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule.rowfilter import org.apache.hadoop.security.UserGroupInformation import org.apache.spark.sql.{Row, SparkSession} @@ -25,10 +25,11 @@ import org.apache.spark.sql.catalyst.rules.Rule import org.apache.spark.sql.execution.command.{RunnableCommand, ShowColumnsCommand} import org.apache.kyuubi.plugin.spark.authz.{ObjectType, OperationType} -import org.apache.kyuubi.plugin.spark.authz.util.{AuthZUtils, ObjectFilterPlaceHolder, WithInternalChildren} +import org.apache.kyuubi.plugin.spark.authz.ranger.{AccessRequest, AccessResource, AccessType, SparkRangerAdminPlugin} +import org.apache.kyuubi.plugin.spark.authz.util.{AuthZUtils, WithInternalChildren} import org.apache.kyuubi.util.reflect.ReflectUtils._ -class RuleReplaceShowObjectCommands extends Rule[LogicalPlan] { +object RuleReplaceShowObjectCommands extends Rule[LogicalPlan] { override def apply(plan: LogicalPlan): LogicalPlan = plan match { case r: RunnableCommand if r.nodeName == "ShowTablesCommand" => FilteredShowTablesCommand(r) case n: LogicalPlan if n.nodeName == "ShowTables" => diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala index 32ad30e211f..c4fd721ca98 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala @@ -43,6 +43,10 @@ trait CommandSpec extends { final def operationType: OperationType = OperationType.withName(opType) } +trait CommandSpecs[T <: CommandSpec] { + def specs: Seq[T] +} + /** * A specification describe a database command * @@ -53,7 +57,8 @@ trait CommandSpec extends { case class DatabaseCommandSpec( classname: String, databaseDescs: Seq[DatabaseDesc], - opType: String = "QUERY") extends CommandSpec {} + opType: String = OperationType.QUERY.toString, + uriDescs: Seq[UriDesc] = Nil) extends CommandSpec {} /** * A specification describe a function command @@ -79,7 +84,8 @@ case class TableCommandSpec( classname: String, tableDescs: Seq[TableDesc], opType: String = OperationType.QUERY.toString, - queryDescs: Seq[QueryDesc] = Nil) extends CommandSpec { + queryDescs: Seq[QueryDesc] = Nil, + uriDescs: Seq[UriDesc] = Nil) extends CommandSpec { def queries: LogicalPlan => Seq[LogicalPlan] = plan => { queryDescs.flatMap { qd => try { @@ -96,7 +102,8 @@ case class TableCommandSpec( case class ScanSpec( classname: String, scanDescs: Seq[ScanDesc], - functionDescs: Seq[FunctionDesc] = Seq.empty) extends CommandSpec { + functionDescs: Seq[FunctionDesc] = Seq.empty, + uriDescs: Seq[UriDesc] = Seq.empty) extends CommandSpec { override def opType: String = OperationType.QUERY.toString def tables: (LogicalPlan, SparkSession) => Seq[Table] = (plan, spark) => { scanDescs.flatMap { td => @@ -110,6 +117,18 @@ case class ScanSpec( } } + def uris: LogicalPlan => Seq[Uri] = plan => { + uriDescs.flatMap { ud => + try { + ud.extract(plan) + } catch { + case e: Exception => + LOG.debug(ud.error(plan, e)) + None + } + } + } + def functions: (Expression) => Seq[Function] = (expr) => { functionDescs.flatMap { fd => try { diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Descriptor.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Descriptor.scala index fc660ce143e..4c0cf2a141d 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Descriptor.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Descriptor.scala @@ -54,11 +54,15 @@ sealed trait Descriptor { */ def extract(v: AnyRef): AnyRef + def comment: String + final def error(v: AnyRef, e: Throwable): String = { val resourceName = getClass.getSimpleName.stripSuffix("Desc") val objectClass = v.getClass.getName s"[Spark$SPARK_VERSION] failed to get $resourceName from $objectClass by" + - s" $fieldExtractor/$fieldName, due to ${e.getMessage}" + s" $fieldExtractor/$fieldName, " + + (if (comment.nonEmpty) s"desc comment: ${comment}") + + s"due to ${e.getMessage}" } } @@ -70,7 +74,8 @@ sealed trait Descriptor { */ case class ColumnDesc( fieldName: String, - fieldExtractor: String) extends Descriptor { + fieldExtractor: String, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Seq[String] = { val columnsVal = invokeAs[AnyRef](v, fieldName) val columnExtractor = lookupExtractor[ColumnExtractor](fieldExtractor) @@ -89,7 +94,8 @@ case class DatabaseDesc( fieldName: String, fieldExtractor: String, catalogDesc: Option[CatalogDesc] = None, - isInput: Boolean = false) extends Descriptor { + isInput: Boolean = false, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Database = { val databaseVal = invokeAs[AnyRef](v, fieldName) val databaseExtractor = lookupExtractor[DatabaseExtractor](fieldExtractor) @@ -113,7 +119,8 @@ case class DatabaseDesc( case class FunctionTypeDesc( fieldName: String, fieldExtractor: String, - skipTypes: Seq[String]) extends Descriptor { + skipTypes: Seq[String], + comment: String = "") extends Descriptor { override def extract(v: AnyRef): FunctionType = { extract(v, SparkSession.active) } @@ -143,7 +150,8 @@ case class FunctionDesc( fieldExtractor: String, databaseDesc: Option[DatabaseDesc] = None, functionTypeDesc: Option[FunctionTypeDesc] = None, - isInput: Boolean = false) extends Descriptor { + isInput: Boolean = false, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Function = { val functionVal = invokeAs[AnyRef](v, fieldName) val functionExtractor = lookupExtractor[FunctionExtractor](fieldExtractor) @@ -168,7 +176,8 @@ case class FunctionDesc( */ case class QueryDesc( fieldName: String, - fieldExtractor: String = "LogicalPlanQueryExtractor") extends Descriptor { + fieldExtractor: String = "LogicalPlanQueryExtractor", + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Option[LogicalPlan] = { val queryVal = invokeAs[AnyRef](v, fieldName) val queryExtractor = lookupExtractor[QueryExtractor](fieldExtractor) @@ -186,7 +195,8 @@ case class QueryDesc( case class TableTypeDesc( fieldName: String, fieldExtractor: String, - skipTypes: Seq[String]) extends Descriptor { + skipTypes: Seq[String], + comment: String = "") extends Descriptor { override def extract(v: AnyRef): TableType = { extract(v, SparkSession.active) } @@ -224,7 +234,8 @@ case class TableDesc( tableTypeDesc: Option[TableTypeDesc] = None, catalogDesc: Option[CatalogDesc] = None, isInput: Boolean = false, - setCurrentDatabaseIfMissing: Boolean = false) extends Descriptor { + setCurrentDatabaseIfMissing: Boolean = false, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Option[Table] = { extract(v, SparkSession.active) } @@ -254,7 +265,8 @@ case class TableDesc( case class ActionTypeDesc( fieldName: String = null, fieldExtractor: String = null, - actionType: Option[String] = None) extends Descriptor { + actionType: Option[String] = None, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): PrivilegeObjectActionType = { actionType.map(PrivilegeObjectActionType.withName).getOrElse { val actionTypeVal = invokeAs[AnyRef](v, fieldName) @@ -272,7 +284,8 @@ case class ActionTypeDesc( */ case class CatalogDesc( fieldName: String = "catalog", - fieldExtractor: String = "CatalogPluginCatalogExtractor") extends Descriptor { + fieldExtractor: String = "CatalogPluginCatalogExtractor", + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Option[String] = { val catalogVal = invokeAs[AnyRef](v, fieldName) val catalogExtractor = lookupExtractor[CatalogExtractor](fieldExtractor) @@ -283,7 +296,8 @@ case class CatalogDesc( case class ScanDesc( fieldName: String, fieldExtractor: String, - catalogDesc: Option[CatalogDesc] = None) extends Descriptor { + catalogDesc: Option[CatalogDesc] = None, + comment: String = "") extends Descriptor { override def extract(v: AnyRef): Option[Table] = { extract(v, SparkSession.active) } @@ -306,3 +320,26 @@ case class ScanDesc( } } } + +/** + * URI Descriptor + * + * @param fieldName the field name or method name of this uri field + * @param fieldExtractor the key of a [[URIExtractor]] instance + * @param isInput read or write + */ +case class UriDesc( + fieldName: String, + fieldExtractor: String, + isInput: Boolean = false, + comment: String = "") extends Descriptor { + override def extract(v: AnyRef): Seq[Uri] = { + extract(v, SparkSession.active) + } + + def extract(v: AnyRef, spark: SparkSession): Seq[Uri] = { + val uriVal = invokeAs[AnyRef](v, fieldName) + val uriExtractor = lookupExtractor[URIExtractor](fieldExtractor) + uriExtractor(spark, uriVal) + } +} diff --git a/kyuubi-server/web-ui/src/router/operation/index.ts b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Uri.scala similarity index 69% rename from kyuubi-server/web-ui/src/router/operation/index.ts rename to extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Uri.scala index 03ba4c28575..aa9af87327d 100644 --- a/kyuubi-server/web-ui/src/router/operation/index.ts +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/Uri.scala @@ -15,17 +15,12 @@ * limitations under the License. */ -const routes = [ - { - path: '/operation/runningJobs', - name: 'operation-runningJobs', - component: () => import('@/views/operation/runningJobs/index.vue') - }, - { - path: '/operation/completedJobs', - name: 'operation-completedJobs', - component: () => import('@/views/operation/completedJobs/index.vue') - } -] +package org.apache.kyuubi.plugin.spark.authz.serde -export default routes +/** + * :: Developer API :: + * + * Represents a URI identity + * @param path + */ +case class Uri(path: String) diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala index 6863516b698..1c5ffb6299a 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala @@ -34,6 +34,7 @@ import org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor.function import org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor.queryExtractors import org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor.tableExtractors import org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor.tableTypeExtractors +import org.apache.kyuubi.plugin.spark.authz.serde.URIExtractor.uriExtractors import org.apache.kyuubi.util.reflect.ReflectUtils._ package object serde { @@ -129,6 +130,7 @@ package object serde { case c if classOf[FunctionExtractor].isAssignableFrom(c) => functionExtractors case c if classOf[FunctionTypeExtractor].isAssignableFrom(c) => functionTypeExtractors case c if classOf[ActionTypeExtractor].isAssignableFrom(c) => actionTypeExtractors + case c if classOf[URIExtractor].isAssignableFrom(c) => uriExtractors case _ => throw new IllegalArgumentException(s"Unknown extractor type: $ct") } extractors(extractorKey).asInstanceOf[T] diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/tableExtractors.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/tableExtractors.scala index 2c212cc5cdb..8a7bc452293 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/tableExtractors.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/tableExtractors.scala @@ -17,8 +17,7 @@ package org.apache.kyuubi.plugin.spark.authz.serde -import java.util.{Map => JMap} -import java.util.LinkedHashMap +import java.util.{LinkedHashMap, Map => JMap} import scala.collection.JavaConverters._ @@ -27,10 +26,13 @@ import org.apache.spark.sql.catalyst.{InternalRow, TableIdentifier} import org.apache.spark.sql.catalyst.catalog.CatalogTable import org.apache.spark.sql.catalyst.expressions.Expression import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, SubqueryAlias} +import org.apache.spark.sql.connector.catalog.Identifier +import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation import org.apache.spark.sql.types.DataType import org.apache.spark.unsafe.types.UTF8String import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ +import org.apache.kyuubi.plugin.spark.authz.util.PathIdentifier._ import org.apache.kyuubi.util.reflect.ReflectUtils._ /** @@ -78,14 +80,28 @@ object TableExtractor { class TableIdentifierTableExtractor extends TableExtractor { override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { val identifier = v1.asInstanceOf[TableIdentifier] - val owner = - try { - val catalogTable = spark.sessionState.catalog.getTableMetadata(identifier) - Option(catalogTable.owner).filter(_.nonEmpty) - } catch { - case _: Exception => None - } - Some(Table(None, identifier.database, identifier.table, owner)) + if (isPathIdentifier(identifier.table, spark)) { + None + } else { + val owner = + try { + val catalogTable = spark.sessionState.catalog.getTableMetadata(identifier) + Option(catalogTable.owner).filter(_.nonEmpty) + } catch { + case _: Exception => None + } + Some(Table(None, identifier.database, identifier.table, owner)) + } + } +} + +/** + * org.apache.spark.sql.catalyst.TableIdentifier Option + */ +class TableIdentifierOptionTableExtractor extends TableExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { + val tableIdentifier = v1.asInstanceOf[Option[TableIdentifier]] + tableIdentifier.flatMap(lookupExtractor[TableIdentifierTableExtractor].apply(spark, _)) } } @@ -133,10 +149,10 @@ class ResolvedTableTableExtractor extends TableExtractor { * org.apache.spark.sql.connector.catalog.Identifier */ class IdentifierTableExtractor extends TableExtractor { - override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { - val namespace = invokeAs[Array[String]](v1, "namespace") - val table = invokeAs[String](v1, "name") - Some(Table(None, Some(quote(namespace)), table, None)) + override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = v1 match { + case identifier: Identifier if !isPathIdentifier(identifier.name(), spark) => + Some(Table(None, Some(quote(identifier.namespace())), identifier.name(), None)) + case _ => None } } @@ -174,18 +190,18 @@ class ExpressionSeqTableExtractor extends TableExtractor { class DataSourceV2RelationTableExtractor extends TableExtractor { override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { val plan = v1.asInstanceOf[LogicalPlan] - val maybeV2Relation = plan.find(_.getClass.getSimpleName == "DataSourceV2Relation") - maybeV2Relation match { - case None => None - case Some(v2Relation) => - val maybeCatalogPlugin = invokeAs[Option[AnyRef]](v2Relation, "catalog") - val maybeCatalog = maybeCatalogPlugin.flatMap(catalogPlugin => + plan.find(_.getClass.getSimpleName == "DataSourceV2Relation").get match { + case v2Relation: DataSourceV2Relation + if v2Relation.identifier.isEmpty || + !isPathIdentifier(v2Relation.identifier.get.name(), spark) => + val maybeCatalog = v2Relation.catalog.flatMap(catalogPlugin => lookupExtractor[CatalogPluginCatalogExtractor].apply(catalogPlugin)) - lookupExtractor[TableTableExtractor].apply(spark, invokeAs[AnyRef](v2Relation, "table")) + lookupExtractor[TableTableExtractor].apply(spark, v2Relation.table) .map { table => val maybeOwner = TableExtractor.getOwner(v2Relation) table.copy(catalog = maybeCatalog, owner = maybeOwner) } + case _ => None } } } @@ -207,12 +223,16 @@ class LogicalRelationTableExtractor extends TableExtractor { */ class ResolvedDbObjectNameTableExtractor extends TableExtractor { override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { - val catalogVal = invokeAs[AnyRef](v1, "catalog") - val catalog = lookupExtractor[CatalogPluginCatalogExtractor].apply(catalogVal) val nameParts = invokeAs[Seq[String]](v1, "nameParts") - val namespace = nameParts.init.toArray val table = nameParts.last - Some(Table(catalog, Some(quote(namespace)), table, None)) + if (isPathIdentifier(table, spark)) { + None + } else { + val catalogVal = invokeAs[AnyRef](v1, "catalog") + val catalog = lookupExtractor[CatalogPluginCatalogExtractor].apply(catalogVal) + val namespace = nameParts.init.toArray + Some(Table(catalog, Some(quote(namespace)), table, None)) + } } } @@ -234,6 +254,25 @@ class ResolvedIdentifierTableExtractor extends TableExtractor { } } +/** + * org.apache.spark.sql.catalyst.plans.logical.SubqueryAlias + */ +class SubqueryAliasTableExtractor extends TableExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Option[Table] = { + v1.asInstanceOf[SubqueryAlias] match { + case SubqueryAlias(_, SubqueryAlias(identifier, _)) => + if (isPathIdentifier(identifier.name, spark)) { + None + } else { + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) + } + case SubqueryAlias(identifier, _) if !isPathIdentifier(identifier.name, spark) => + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) + case _ => None + } + } +} + /** * org.apache.spark.sql.connector.catalog.Table */ @@ -249,10 +288,11 @@ class HudiDataSourceV2RelationTableExtractor extends TableExtractor { invokeAs[LogicalPlan](v1, "table") match { // Match multipartIdentifier with tableAlias case SubqueryAlias(_, SubqueryAlias(identifier, _)) => - new StringTableExtractor().apply(spark, identifier.toString()) + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) // Match multipartIdentifier without tableAlias case SubqueryAlias(identifier, _) => - new StringTableExtractor().apply(spark, identifier.toString()) + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) + case _ => None } } } @@ -262,10 +302,11 @@ class HudiMergeIntoTargetTableExtractor extends TableExtractor { invokeAs[LogicalPlan](v1, "targetTable") match { // Match multipartIdentifier with tableAlias case SubqueryAlias(_, SubqueryAlias(identifier, relation)) => - new StringTableExtractor().apply(spark, identifier.toString()) + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) // Match multipartIdentifier without tableAlias case SubqueryAlias(identifier, _) => - new StringTableExtractor().apply(spark, identifier.toString()) + lookupExtractor[StringTableExtractor].apply(spark, identifier.toString()) + case _ => None } } } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/uriExtractors.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/uriExtractors.scala new file mode 100644 index 00000000000..434cc769927 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/uriExtractors.scala @@ -0,0 +1,159 @@ +/* + * 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.kyuubi.plugin.spark.authz.serde + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.catalyst.TableIdentifier +import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, CatalogTable} +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, SubqueryAlias} +import org.apache.spark.sql.connector.catalog.Identifier +import org.apache.spark.sql.execution.datasources.HadoopFsRelation +import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation + +import org.apache.kyuubi.plugin.spark.authz.util.PathIdentifier._ +import org.apache.kyuubi.util.reflect.ReflectUtils.invokeAs + +trait URIExtractor extends ((SparkSession, AnyRef) => Seq[Uri]) with Extractor + +object URIExtractor { + val uriExtractors: Map[String, URIExtractor] = { + loadExtractorsToMap[URIExtractor] + } +} + +/** + * String + */ +class StringURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1 match { + case uriPath: String => Seq(Uri(uriPath)) + case Some(uriPath: String) => Seq(Uri(uriPath)) + case _ => Nil + } + } +} + +class StringSeqURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[Seq[String]].map(Uri) + } +} + +class CatalogStorageFormatURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[CatalogStorageFormat].locationUri.map(uri => Uri(uri.getPath)).toSeq + } +} + +class PropertiesPathUriExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[Map[String, String]].get("path").map(Uri).toSeq + } +} + +class PropertiesLocationUriExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[Map[String, String]].get("location").map(Uri).toSeq + } +} + +class BaseRelationFileIndexURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1 match { + case h: HadoopFsRelation => h.location.rootPaths.map(_.toString).map(Uri) + case _ => Nil + } + } +} + +class TableSpecURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + new StringURIExtractor().apply(spark, invokeAs[Option[String]](v1, "location")) + } +} + +class CatalogTableURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[CatalogTable].storage.locationUri.map(_.toString).map(Uri).toSeq + } +} + +class PartitionLocsSeqURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + v1.asInstanceOf[Seq[(_, Option[String])]].flatMap(_._2).map(Uri) + } +} + +class IdentifierURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = v1 match { + case identifier: Identifier if isPathIdentifier(identifier.name(), spark) => + Seq(identifier.name()).map(Uri) + case _ => Nil + } +} + +class SubqueryAliasURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = v1 match { + case SubqueryAlias(_, SubqueryAlias(identifier, _)) => + if (isPathIdentifier(identifier.name, spark)) { + Seq(identifier.name).map(Uri) + } else { + Nil + } + case SubqueryAlias(identifier, _) if isPathIdentifier(identifier.name, spark) => + Seq(identifier.name).map(Uri) + case _ => Nil + } +} + +class DataSourceV2RelationURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + val plan = v1.asInstanceOf[LogicalPlan] + plan.find(_.getClass.getSimpleName == "DataSourceV2Relation").get match { + case v2Relation: DataSourceV2Relation + if v2Relation.identifier.isDefined && + isPathIdentifier(v2Relation.identifier.get.name, spark) => + Seq(v2Relation.identifier.get.name).map(Uri) + case _ => Nil + } + } +} + +class ResolvedTableURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = { + val identifier = invokeAs[AnyRef](v1, "identifier") + lookupExtractor[IdentifierURIExtractor].apply(spark, identifier) + } +} + +class TableIdentifierURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = v1 match { + case tableIdentifier: TableIdentifier if isPathIdentifier(tableIdentifier.table, spark) => + Seq(tableIdentifier.table).map(Uri) + case _ => Nil + } +} + +class TableIdentifierOptionURIExtractor extends URIExtractor { + override def apply(spark: SparkSession, v1: AnyRef): Seq[Uri] = v1 match { + case Some(tableIdentifier: TableIdentifier) => + lookupExtractor[TableIdentifierURIExtractor].apply(spark, tableIdentifier) + case _ => Nil + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PathIdentifier.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PathIdentifier.scala new file mode 100644 index 00000000000..2666c37c3d3 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PathIdentifier.scala @@ -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. + */ + +package org.apache.kyuubi.plugin.spark.authz.util + +import java.io.File + +import org.apache.spark.sql.SparkSession + +/** + * An object for handling table access on path-based table. This is a stop-gap solution + * until PathIdentifiers are implemented in Apache Spark. + */ +object PathIdentifier { + def isPathIdentifier(path: String, spark: SparkSession): Boolean = + spark.sessionState.conf.runSQLonFile && path != null && path.startsWith(File.separator) +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ReservedKeys.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ReservedKeys.scala index 60d9898452d..81259be2a0e 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ReservedKeys.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/ReservedKeys.scala @@ -22,4 +22,5 @@ object ReservedKeys { final val KYUUBI_SESSION_USER = "kyuubi.session.user" final val KYUUBI_SESSION_SIGN_PUBLICKEY = "kyuubi.session.sign.publickey" final val KYUUBI_SESSION_USER_SIGN = "kyuubi.session.user.sign" + final var KYUUBI_EXPLAIN_COMMAND_EXECUTION_ID = "kyuubi.authz.command.explain.executionid" } diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/WithInternalChildren.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/WithInternalChildren.scala index bbce1dff89e..582b34abee4 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/WithInternalChildren.scala +++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/WithInternalChildren.scala @@ -17,6 +17,7 @@ package org.apache.kyuubi.plugin.spark.authz.util +import org.apache.spark.sql.catalyst.expressions.Expression import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan trait WithInternalChildren { @@ -26,3 +27,7 @@ trait WithInternalChildren { trait WithInternalChild { def withNewChildInternal(newChild: LogicalPlan): LogicalPlan } + +trait WithInternalExpressionChild { + def withNewChildInternal(newChild: Expression): Expression +} diff --git a/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala b/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala index 7faddd0c7fa..afc7a5fde53 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala @@ -27,9 +27,9 @@ import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.ranger.plugin.model.RangerPolicy +// scalastyle:off import org.scalatest.funsuite.AnyFunSuite -// scalastyle:off import org.apache.kyuubi.plugin.spark.authz.RangerTestNamespace._ import org.apache.kyuubi.plugin.spark.authz.RangerTestUsers._ import org.apache.kyuubi.plugin.spark.authz.gen.KRangerPolicyItemAccess.allowTypes @@ -37,6 +37,7 @@ import org.apache.kyuubi.plugin.spark.authz.gen.KRangerPolicyResource._ import org.apache.kyuubi.plugin.spark.authz.gen.RangerAccessType._ import org.apache.kyuubi.plugin.spark.authz.gen.RangerClassConversions._ import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ /** * Generates the policy file to test/main/resources dir. @@ -59,11 +60,9 @@ class PolicyJsonFileGenerator extends AnyFunSuite { .build() test("check ranger policy file") { - val pluginHome = getClass.getProtectionDomain.getCodeSource.getLocation.getPath - .split("target").head val policyFileName = "sparkSql_hive_jenkins.json" - val policyFilePath = - Paths.get(pluginHome, "src", "test", "resources", policyFileName) + val policyFilePath = Paths.get( + s"${getCurrentModuleHome(this)}/src/test/resources/$policyFileName") val generatedStr = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(servicePolicies) @@ -108,6 +107,7 @@ class PolicyJsonFileGenerator extends AnyFunSuite { policyAccessForDefaultBobUse, policyAccessForDefaultBobSelect, policyAccessForPermViewAccessOnly, + policyAccessForTable2AccessOnly, // row filter policyFilterForSrcTableKeyLessThan20, policyFilterForPermViewKeyLessThan20, @@ -345,4 +345,16 @@ class PolicyJsonFileGenerator extends AnyFunSuite { users = List(permViewOnlyUser), accesses = allowTypes(select), delegateAdmin = true))) + + private val policyAccessForTable2AccessOnly = KRangerPolicy( + name = "someone_access_table2", + resources = Map( + databaseRes(defaultDb), + tableRes("table2"), + allColumnRes), + policyItems = List( + KRangerPolicyItem( + users = List(table2OnlyUser), + accesses = allowTypes(select), + delegateAdmin = true))) } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/resources/sparkSql_hive_jenkins.json b/extensions/spark/kyuubi-spark-authz/src/test/resources/sparkSql_hive_jenkins.json index 6c160d3216a..76d8c788a22 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/resources/sparkSql_hive_jenkins.json +++ b/extensions/spark/kyuubi-spark-authz/src/test/resources/sparkSql_hive_jenkins.json @@ -544,6 +544,55 @@ "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", + "name" : "someone_access_table2", + "policyType" : 0, + "policyPriority" : 0, + "description" : "", + "isAuditEnabled" : true, + "resources" : { + "database" : { + "values" : [ "default" ], + "isExcludes" : false, + "isRecursive" : false + }, + "column" : { + "values" : [ "*" ], + "isExcludes" : false, + "isRecursive" : false + }, + "table" : { + "values" : [ "table2" ], + "isExcludes" : false, + "isRecursive" : false + } + }, + "conditions" : [ ], + "policyItems" : [ { + "accesses" : [ { + "type" : "select", + "isAllowed" : true + } ], + "users" : [ "user_table2_only" ], + "groups" : [ ], + "roles" : [ ], + "conditions" : [ ], + "delegateAdmin" : true + } ], + "denyPolicyItems" : [ ], + "allowExceptions" : [ ], + "denyExceptions" : [ ], + "dataMaskPolicyItems" : [ ], + "rowFilterPolicyItems" : [ ], + "options" : { }, + "validitySchedules" : [ ], + "policyLabels" : [ ], + "isDenyAllElse" : false + }, { + "id" : 9, + "guid" : "45c48cce-2e2d-3fbd-aa1a-fc51c7c6ad26", + "isEnabled" : true, + "version" : 1, + "service" : "hive_jenkins", "name" : "src_key_less_than_20", "policyType" : 2, "policyPriority" : 0, @@ -586,8 +635,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 9, - "guid" : "45c48cce-2e2d-3fbd-aa1a-fc51c7c6ad26", + "id" : 10, + "guid" : "d3d94468-02a4-3259-b55d-38e6d163e820", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -633,8 +682,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 10, - "guid" : "d3d94468-02a4-3259-b55d-38e6d163e820", + "id" : 11, + "guid" : "6512bd43-d9ca-36e0-ac99-0b0a82652dca", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -685,8 +734,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 11, - "guid" : "6512bd43-d9ca-36e0-ac99-0b0a82652dca", + "id" : 12, + "guid" : "c20ad4d7-6fe9-3759-aa27-a0c99bff6710", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -737,8 +786,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 12, - "guid" : "c20ad4d7-6fe9-3759-aa27-a0c99bff6710", + "id" : 13, + "guid" : "c51ce410-c124-310e-8db5-e4b97fc2af39", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -789,8 +838,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 13, - "guid" : "c51ce410-c124-310e-8db5-e4b97fc2af39", + "id" : 14, + "guid" : "aab32389-22bc-325a-af60-6eb525ffdc56", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -841,8 +890,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 14, - "guid" : "aab32389-22bc-325a-af60-6eb525ffdc56", + "id" : 15, + "guid" : "9bf31c7f-f062-336a-96d3-c8bd1f8f2ff3", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", @@ -893,8 +942,8 @@ "policyLabels" : [ ], "isDenyAllElse" : false }, { - "id" : 15, - "guid" : "9bf31c7f-f062-336a-96d3-c8bd1f8f2ff3", + "id" : 16, + "guid" : "c74d97b0-1eae-357e-84aa-9d5bade97baf", "isEnabled" : true, "version" : 1, "service" : "hive_jenkins", diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilderSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilderSuite.scala index 54b91eb2837..214a0375485 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilderSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/PrivilegesBuilderSuite.scala @@ -306,17 +306,26 @@ abstract class PrivilegesBuilderSuite extends AnyFunSuite val (in, out, operationType) = PrivilegesBuilder.build(plan, spark) assert(in.isEmpty) - assert(out.size === 1) - val po = out.head - assert(po.actionType === PrivilegeObjectActionType.OTHER) - assert(po.privilegeObjectType === PrivilegeObjectType.TABLE_OR_VIEW) - assert(po.catalog.isEmpty) - assertEqualsIgnoreCase(reusedDb)(po.dbname) - assertEqualsIgnoreCase(reusedPartTableShort)(po.objectName) - assert(po.columns.head === "pid") - checkTableOwner(po) - val accessType = ranger.AccessType(po, operationType, isInput = false) - assert(accessType === AccessType.ALTER) + assert(out.size === 2) + val po0 = out.head + assert(po0.actionType === PrivilegeObjectActionType.OTHER) + assert(po0.privilegeObjectType === PrivilegeObjectType.TABLE_OR_VIEW) + assert(po0.catalog.isEmpty) + assertEqualsIgnoreCase(reusedDb)(po0.dbname) + assertEqualsIgnoreCase(reusedPartTableShort)(po0.objectName) + assert(po0.columns.head === "pid") + checkTableOwner(po0) + val accessType0 = ranger.AccessType(po0, operationType, isInput = false) + assert(accessType0 === AccessType.ALTER) + + val po1 = out.last + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.catalog.isEmpty) + assert(po1.dbname === newLoc) + assert(po1.columns === Seq.empty) + checkTableOwner(po1) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 === AccessType.WRITE) } test("AlterTable(Un)SetPropertiesCommand") { @@ -1292,16 +1301,25 @@ class InMemoryPrivilegeBuilderSuite extends PrivilegesBuilderSuite { "org.apache.spark.sql.execution.command.AlterDatabaseSetLocationCommand") assert(operationType === ALTERDATABASE_LOCATION) assert(in.isEmpty) - assert(out.size === 1) - val po = out.head - assert(po.actionType === PrivilegeObjectActionType.OTHER) - assert(po.privilegeObjectType === PrivilegeObjectType.DATABASE) - assert(po.catalog.isEmpty) - assertEqualsIgnoreCase(defaultDb)(po.dbname) - assertEqualsIgnoreCase(defaultDb)(po.objectName) - assert(po.columns.isEmpty) - val accessType = ranger.AccessType(po, operationType, isInput = false) - assert(accessType === AccessType.ALTER) + assert(out.size === 2) + val po0 = out.head + assert(po0.actionType === PrivilegeObjectActionType.OTHER) + assert(po0.privilegeObjectType === PrivilegeObjectType.DATABASE) + assert(po0.catalog.isEmpty) + assertEqualsIgnoreCase(defaultDb)(po0.dbname) + assertEqualsIgnoreCase(defaultDb)(po0.objectName) + assert(po0.columns.isEmpty) + val accessType0 = ranger.AccessType(po0, operationType, isInput = false) + assert(accessType0 === AccessType.ALTER) + + val po1 = out.last + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.catalog.isEmpty) + assertEqualsIgnoreCase(defaultDb)(po0.dbname) + assertEqualsIgnoreCase(defaultDb)(po0.objectName) + assert(po1.columns.isEmpty) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 === AccessType.WRITE) } test("CreateDataSourceTableAsSelectCommand") { @@ -1430,18 +1448,27 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { .queryExecution.analyzed val (in, out, operationType) = PrivilegesBuilder.build(plan, spark) assert(operationType === LOAD) - assert(in.isEmpty) - - assert(out.size === 1) - val po0 = out.head - assert(po0.actionType === PrivilegeObjectActionType.INSERT_OVERWRITE) - assert(po0.privilegeObjectType === PrivilegeObjectType.TABLE_OR_VIEW) - assertEqualsIgnoreCase(reusedDb)(po0.dbname) - assert(po0.objectName equalsIgnoreCase tableName.split("\\.").last) + assert(in.size === 1) + val po0 = in.head + assert(po0.actionType === PrivilegeObjectActionType.OTHER) + assert(po0.privilegeObjectType === PrivilegeObjectType.DFS_URI) + assert(po0.dbname === dataPath) + assert(po0.objectName === null) assert(po0.columns.isEmpty) checkTableOwner(po0) - val accessType0 = ranger.AccessType(po0, operationType, isInput = false) - assert(accessType0 === AccessType.UPDATE) + val accessType0 = ranger.AccessType(po0, operationType, isInput = true) + assert(accessType0 === AccessType.READ) + + assert(out.size === 1) + val po1 = out.head + assert(po1.actionType === PrivilegeObjectActionType.INSERT_OVERWRITE) + assert(po1.privilegeObjectType === PrivilegeObjectType.TABLE_OR_VIEW) + assertEqualsIgnoreCase(reusedDb)(po1.dbname) + assert(po1.objectName equalsIgnoreCase tableName.split("\\.").last) + assert(po1.columns.isEmpty) + checkTableOwner(po1) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 === AccessType.UPDATE) } } @@ -1450,7 +1477,7 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val directory = File(tableDirectory).createDirectory() val plan = sql( s""" - |INSERT OVERWRITE DIRECTORY '$directory.path' + |INSERT OVERWRITE DIRECTORY '${directory.path}' |USING parquet |SELECT * FROM $reusedPartTable""".stripMargin) .queryExecution.analyzed @@ -1467,7 +1494,15 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val accessType0 = ranger.AccessType(po0, operationType, isInput = true) assert(accessType0 === AccessType.SELECT) - assert(out.isEmpty) + assert(out.size == 1) + val po1 = out.head + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.privilegeObjectType === PrivilegeObjectType.DFS_URI) + assert(po1.dbname === directory.path) + assert(po1.objectName === null) + assert(po1.columns === Seq.empty) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 == AccessType.WRITE) } test("InsertIntoDataSourceCommand") { @@ -1523,7 +1558,6 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { checkTableOwner(po) val accessType = ranger.AccessType(po, operationType, isInput = false) assert(accessType === AccessType.UPDATE) - } } } @@ -1574,7 +1608,7 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val directory = File(tableDirectory).createDirectory() val plan = sql( s""" - |INSERT OVERWRITE DIRECTORY '$directory.path' + |INSERT OVERWRITE DIRECTORY '${directory.path}' |USING parquet |SELECT * FROM $reusedPartTable""".stripMargin) .queryExecution.analyzed @@ -1591,7 +1625,15 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val accessType0 = ranger.AccessType(po0, operationType, isInput = true) assert(accessType0 === AccessType.SELECT) - assert(out.isEmpty) + assert(out.size == 1) + val po1 = out.head + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.privilegeObjectType === PrivilegeObjectType.DFS_URI) + assert(po1.dbname === directory.path) + assert(po1.objectName === null) + assert(po1.columns === Seq.empty) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 == AccessType.WRITE) } test("InsertIntoHiveDirCommand") { @@ -1599,7 +1641,7 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val directory = File(tableDirectory).createDirectory() val plan = sql( s""" - |INSERT OVERWRITE DIRECTORY '$directory.path' + |INSERT OVERWRITE DIRECTORY '${directory.path}' |ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' |SELECT * FROM $reusedPartTable""".stripMargin) .queryExecution.analyzed @@ -1616,7 +1658,15 @@ class HiveCatalogPrivilegeBuilderSuite extends PrivilegesBuilderSuite { val accessType0 = ranger.AccessType(po0, operationType, isInput = true) assert(accessType0 === AccessType.SELECT) - assert(out.isEmpty) + assert(out.size == 1) + val po1 = out.head + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.privilegeObjectType === PrivilegeObjectType.DFS_URI) + assert(po1.dbname === directory.path) + assert(po1.objectName === null) + assert(po1.columns === Seq.empty) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 == AccessType.WRITE) } test("InsertIntoHiveTableCommand") { diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/RangerTestResources.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/RangerTestResources.scala index 0b1df64da78..4f870d504f5 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/RangerTestResources.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/RangerTestResources.scala @@ -28,6 +28,7 @@ object RangerTestUsers { val createOnlyUser = "create_only_user" val defaultTableOwner = "default_table_owner" val permViewOnlyUser = "user_perm_view_only" + val table2OnlyUser = "user_table2_only" // non-authorized users val invisibleUser = "i_am_invisible" @@ -41,6 +42,7 @@ object RangerTestNamespace { val sparkCatalog = "spark_catalog" val icebergNamespace = "iceberg_ns" val hudiNamespace = "hudi_ns" + val deltaNamespace = "delta_ns" val namespace1 = "ns1" val namespace2 = "ns2" } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/SparkSessionProvider.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/SparkSessionProvider.scala index c7e541ef525..7aa4d99e45c 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/SparkSessionProvider.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/SparkSessionProvider.scala @@ -28,6 +28,7 @@ import org.scalatest.Assertions._ import org.apache.kyuubi.Utils import org.apache.kyuubi.plugin.spark.authz.RangerTestUsers._ import org.apache.kyuubi.plugin.spark.authz.V2JdbcTableCatalogPrivilegesBuilderSuite._ +import org.apache.kyuubi.plugin.spark.authz.ranger.DeltaCatalogRangerSparkExtensionSuite._ import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ trait SparkSessionProvider { @@ -106,7 +107,7 @@ trait SparkSessionProvider { } private def isCatalogSupportPurge(catalogName: String): Boolean = { - val unsupportedCatalogs = Set(v2JdbcTableCatalogClassName) + val unsupportedCatalogs = Set(v2JdbcTableCatalogClassName, deltaCatalogClassName) spark.conf.getOption(s"spark.sql.catalog.$catalogName") match { case Some(catalog) if !unsupportedCatalogs.contains(catalog) => true case _ => false diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/V2CommandsPrivilegesSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/V2CommandsPrivilegesSuite.scala index 149c9ba8f6b..62b7939b3cb 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/V2CommandsPrivilegesSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/V2CommandsPrivilegesSuite.scala @@ -738,16 +738,25 @@ abstract class V2CommandsPrivilegesSuite extends PrivilegesBuilderSuite { "org.apache.spark.sql.catalyst.plans.logical.SetNamespaceLocation") assert(operationType === ALTERDATABASE_LOCATION) assert(in.isEmpty) - assert(out.size === 1) - val po = out.head - assert(po.actionType === PrivilegeObjectActionType.OTHER) - assert(po.privilegeObjectType === PrivilegeObjectType.DATABASE) - assert(po.catalog.get === sparkSessionCatalogName) - assertEqualsIgnoreCase(defaultDb)(po.dbname) - assertEqualsIgnoreCase(defaultDb)(po.objectName) - assert(po.columns.isEmpty) - val accessType = ranger.AccessType(po, operationType, isInput = false) - assert(accessType === AccessType.ALTER) + assert(out.size === 2) + val po0 = out.head + assert(po0.actionType === PrivilegeObjectActionType.OTHER) + assert(po0.privilegeObjectType === PrivilegeObjectType.DATABASE) + assert(po0.catalog.get === sparkSessionCatalogName) + assertEqualsIgnoreCase(defaultDb)(po0.dbname) + assertEqualsIgnoreCase(defaultDb)(po0.objectName) + assert(po0.columns.isEmpty) + val accessType0 = ranger.AccessType(po0, operationType, isInput = false) + assert(accessType0 === AccessType.ALTER) + + val po1 = out.last + assert(po1.actionType === PrivilegeObjectActionType.OTHER) + assert(po1.catalog.isEmpty) + assertEqualsIgnoreCase(defaultDb)(po0.dbname) + assertEqualsIgnoreCase(defaultDb)(po0.objectName) + assert(po1.columns.isEmpty) + val accessType1 = ranger.AccessType(po1, operationType, isInput = false) + assert(accessType1 === AccessType.WRITE) } test("DescribeNamespace") { diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/CheckAuthzExtractorSPISuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/CheckAuthzExtractorSPISuite.scala new file mode 100644 index 00000000000..7a66e99eafa --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/CheckAuthzExtractorSPISuite.scala @@ -0,0 +1,49 @@ +/* + * 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.kyuubi.plugin.spark.authz.gen + +import java.nio.file.Paths + +// scalastyle:off +import org.scalatest.funsuite.AnyFunSuite + +import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ + +class CheckAuthzExtractorSPISuite extends AnyFunSuite { + // scalastyle:on + + test("check authz extractor SPI service file sorted") { + Seq( + "org.apache.kyuubi.plugin.spark.authz.serde.ActionTypeExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.CatalogExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.ColumnExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.DatabaseExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.FunctionExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.FunctionTypeExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.QueryExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.TableExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.TableTypeExtractor", + "org.apache.kyuubi.plugin.spark.authz.serde.URIExtractor") + .foreach { fileName => + val filePath = Paths.get( + s"${getCurrentModuleHome(this)}/src/main/resources/META-INF/services/$fileName") + assertFileContentSorted(filePath) + } + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DatabaseCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DatabaseCommands.scala index a61c142edb5..ebaddf6228a 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DatabaseCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DatabaseCommands.scala @@ -20,7 +20,23 @@ package org.apache.kyuubi.plugin.spark.authz.gen import org.apache.kyuubi.plugin.spark.authz.OperationType._ import org.apache.kyuubi.plugin.spark.authz.serde._ -object DatabaseCommands { +object DatabaseCommands extends CommandSpecs[DatabaseCommandSpec] { + + val CreateDatabaseCommand = { + DatabaseCommandSpec( + "org.apache.spark.sql.execution.command.CreateDatabaseCommand", + Seq(DatabaseDesc("databaseName", classOf[StringDatabaseExtractor])), + CREATEDATABASE, + Seq(UriDesc("path", classOf[StringURIExtractor]))) + } + + val AlterDatabaseSetLocationCommand = { + DatabaseCommandSpec( + "org.apache.spark.sql.execution.command.AlterDatabaseSetLocationCommand", + Seq(DatabaseDesc("databaseName", classOf[StringDatabaseExtractor])), + ALTERDATABASE_LOCATION, + Seq(UriDesc("location", classOf[StringURIExtractor]))) + } val AlterDatabaseProperties = { DatabaseCommandSpec( @@ -47,7 +63,8 @@ object DatabaseCommands { DatabaseCommandSpec( "org.apache.spark.sql.catalyst.plans.logical.SetNamespaceLocation", Seq(DatabaseDesc("namespace", classOf[ResolvedNamespaceDatabaseExtractor])), - ALTERDATABASE_LOCATION) + ALTERDATABASE_LOCATION, + Seq(UriDesc("location", classOf[StringURIExtractor]))) } val CreateNamespace = { @@ -62,7 +79,8 @@ object DatabaseCommands { DatabaseCommandSpec( "org.apache.spark.sql.catalyst.plans.logical.CreateNamespace", Seq(databaseDesc1, databaseDesc2, databaseDesc3), - CREATEDATABASE) + CREATEDATABASE, + Seq(UriDesc("properties", classOf[PropertiesLocationUriExtractor]))) } val DropNamespace = { @@ -141,18 +159,14 @@ object DatabaseCommands { DatabaseCommandSpec(cmd, Seq(databaseDesc), DESCDATABASE) } - val data: Array[DatabaseCommandSpec] = Array( + override def specs: Seq[DatabaseCommandSpec] = Seq( AlterDatabaseProperties, - AlterDatabaseProperties.copy( - classname = "org.apache.spark.sql.execution.command.AlterDatabaseSetLocationCommand", - opType = ALTERDATABASE_LOCATION), - AlterDatabaseProperties.copy( - classname = "org.apache.spark.sql.execution.command.CreateDatabaseCommand", - opType = CREATEDATABASE), + AlterDatabaseSetLocationCommand, AlterDatabaseProperties.copy( classname = "org.apache.spark.sql.execution.command.DropDatabaseCommand", opType = DROPDATABASE), AnalyzeTables, + CreateDatabaseCommand, CreateNamespace, CommentOnNamespace, DescribeDatabase, diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DeltaCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DeltaCommands.scala new file mode 100644 index 00000000000..12f434a50c1 --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/DeltaCommands.scala @@ -0,0 +1,79 @@ +/* + * 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.kyuubi.plugin.spark.authz.gen + +import org.apache.kyuubi.plugin.spark.authz.OperationType._ +import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType._ +import org.apache.kyuubi.plugin.spark.authz.serde._ + +object DeltaCommands extends CommandSpecs[TableCommandSpec] { + + val DeleteCommand = { + val cmd = "org.apache.spark.sql.delta.commands.DeleteCommand" + val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE), comment = "Delta") + val tableDesc = TableDesc( + "target", + classOf[SubqueryAliasTableExtractor], + actionTypeDesc = Some(actionTypeDesc), + comment = "Delta") + val uriDescs = Seq(UriDesc("target", classOf[SubqueryAliasURIExtractor], comment = "Delta")) + TableCommandSpec(cmd, Seq(tableDesc), uriDescs = uriDescs) + } + + val UpdateCommand = { + val cmd = "org.apache.spark.sql.delta.commands.UpdateCommand" + DeleteCommand.copy(classname = cmd) + } + + val MergeIntoCommand = { + val cmd = "org.apache.spark.sql.delta.commands.MergeIntoCommand" + val queryDesc = QueryDesc("source", comment = "Delta") + DeleteCommand.copy(classname = cmd, queryDescs = Seq(queryDesc)) + } + + val OptimizeTableCommand = { + val cmd = "org.apache.spark.sql.delta.commands.OptimizeTableCommand" + val childDesc = TableDesc("child", classOf[ResolvedTableTableExtractor], comment = "Delta") + val tableDesc = + TableDesc("tableId", classOf[TableIdentifierOptionTableExtractor], comment = "Delta") + val uriDescs = Seq( + UriDesc("child", classOf[ResolvedTableURIExtractor], comment = "Delta"), + UriDesc("tableId", classOf[TableIdentifierOptionURIExtractor], comment = "Delta"), + UriDesc("path", classOf[StringURIExtractor], comment = "Delta")) + TableCommandSpec(cmd, Seq(childDesc, tableDesc), ALTERTABLE_COMPACT, uriDescs = uriDescs) + } + + val VacuumTableCommand = { + val cmd = "io.delta.tables.execution.VacuumTableCommand" + val childDesc = TableDesc("child", classOf[ResolvedTableTableExtractor], comment = "Delta") + val tableDesc = + TableDesc("table", classOf[TableIdentifierOptionTableExtractor], comment = "Delta") + val uriDescs = Seq( + UriDesc("child", classOf[ResolvedTableURIExtractor], comment = "Delta"), + UriDesc("table", classOf[TableIdentifierOptionURIExtractor], comment = "Delta"), + UriDesc("path", classOf[StringURIExtractor], comment = "Delta")) + TableCommandSpec(cmd, Seq(childDesc, tableDesc), MSCK, uriDescs = uriDescs) + } + + override def specs: Seq[TableCommandSpec] = Seq( + DeleteCommand, + MergeIntoCommand, + OptimizeTableCommand, + UpdateCommand, + VacuumTableCommand) +} diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/FunctionCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/FunctionCommands.scala index 1822e80fc8a..d5c849dd6bf 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/FunctionCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/FunctionCommands.scala @@ -21,7 +21,7 @@ import org.apache.kyuubi.plugin.spark.authz.OperationType._ import org.apache.kyuubi.plugin.spark.authz.serde._ import org.apache.kyuubi.plugin.spark.authz.serde.FunctionType.{SYSTEM, TEMP} -object FunctionCommands { +object FunctionCommands extends CommandSpecs[FunctionCommandSpec] { val CreateFunction = { val cmd = "org.apache.spark.sql.execution.command.CreateFunctionCommand" @@ -83,9 +83,9 @@ object FunctionCommands { FunctionCommandSpec(cmd, Seq(functionDesc), RELOADFUNCTION) } - val data: Array[FunctionCommandSpec] = Array( + override def specs: Seq[FunctionCommandSpec] = Seq( CreateFunction, DropFunction, DescribeFunction, - RefreshFunction).sortBy(_.classname) + RefreshFunction) } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/HudiCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/HudiCommands.scala index 9b843b1f600..87fc509b5d0 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/HudiCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/HudiCommands.scala @@ -22,27 +22,41 @@ import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType._ import org.apache.kyuubi.plugin.spark.authz.serde._ import org.apache.kyuubi.plugin.spark.authz.serde.TableType._ -object HudiCommands { +object HudiCommands extends CommandSpecs[TableCommandSpec] { val AlterHoodieTableAddColumnsCommand = { val cmd = "org.apache.spark.sql.hudi.command.AlterHoodieTableAddColumnsCommand" - val columnDesc = ColumnDesc("colsToAdd", classOf[StructFieldSeqColumnExtractor]) - val tableDesc = TableDesc("tableId", classOf[TableIdentifierTableExtractor], Some(columnDesc)) + val columnDesc = + ColumnDesc("colsToAdd", classOf[StructFieldSeqColumnExtractor], comment = "Hudi") + val tableDesc = TableDesc( + "tableId", + classOf[TableIdentifierTableExtractor], + Some(columnDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_ADDCOLS) } val AlterHoodieTableChangeColumnCommand = { val cmd = "org.apache.spark.sql.hudi.command.AlterHoodieTableChangeColumnCommand" - val columnDesc = ColumnDesc("columnName", classOf[StringColumnExtractor]) + val columnDesc = ColumnDesc("columnName", classOf[StringColumnExtractor], comment = "Hudi") val tableDesc = - TableDesc("tableIdentifier", classOf[TableIdentifierTableExtractor], Some(columnDesc)) + TableDesc( + "tableIdentifier", + classOf[TableIdentifierTableExtractor], + Some(columnDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_REPLACECOLS) } val AlterHoodieTableDropPartitionCommand = { val cmd = "org.apache.spark.sql.hudi.command.AlterHoodieTableDropPartitionCommand" - val columnDesc = ColumnDesc("partitionSpecs", classOf[PartitionSeqColumnExtractor]) + val columnDesc = + ColumnDesc("partitionSpecs", classOf[PartitionSeqColumnExtractor], comment = "Hudi") val tableDesc = - TableDesc("tableIdentifier", classOf[TableIdentifierTableExtractor], Some(columnDesc)) + TableDesc( + "tableIdentifier", + classOf[TableIdentifierTableExtractor], + Some(columnDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_DROPPARTS) } @@ -52,30 +66,32 @@ object HudiCommands { TableTypeDesc( "oldName", classOf[TableIdentifierTableTypeExtractor], - Seq(TEMP_VIEW)) + Seq(TEMP_VIEW), + comment = "Hudi") val oldTableD = TableDesc( "oldName", classOf[TableIdentifierTableExtractor], - tableTypeDesc = Some(oldTableTableTypeDesc)) + tableTypeDesc = Some(oldTableTableTypeDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(oldTableD), ALTERTABLE_RENAME) } val AlterTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.AlterTableCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], None) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], None, comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_PROPERTIES) } val Spark31AlterTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.Spark31AlterTableCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], None) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], None, comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_PROPERTIES) } val CreateHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.CreateHoodieTableCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor]) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE) } @@ -92,12 +108,14 @@ object HudiCommands { val tableDesc1 = TableDesc( "targetTable", classOf[TableIdentifierTableExtractor], - setCurrentDatabaseIfMissing = true) + setCurrentDatabaseIfMissing = true, + comment = "Hudi") val tableDesc2 = TableDesc( "sourceTable", classOf[TableIdentifierTableExtractor], isInput = true, - setCurrentDatabaseIfMissing = true) + setCurrentDatabaseIfMissing = true, + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc1, tableDesc2), CREATETABLE) } @@ -107,7 +125,8 @@ object HudiCommands { TableTypeDesc( "tableIdentifier", classOf[TableIdentifierTableTypeExtractor], - Seq(TEMP_VIEW)) + Seq(TEMP_VIEW), + comment = "Hudi") TableCommandSpec( cmd, Seq(TableDesc( @@ -124,48 +143,68 @@ object HudiCommands { val TruncateHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.TruncateHoodieTableCommand" - val columnDesc = ColumnDesc("partitionSpec", classOf[PartitionOptionColumnExtractor]) + val columnDesc = + ColumnDesc("partitionSpec", classOf[PartitionOptionColumnExtractor], comment = "Hudi") val tableDesc = TableDesc( "tableIdentifier", classOf[TableIdentifierTableExtractor], - columnDesc = Some(columnDesc)) + columnDesc = Some(columnDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), TRUNCATETABLE) } val CompactionHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.CompactionHoodieTableCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor]) - TableCommandSpec(cmd, Seq(tableDesc, tableDesc.copy(isInput = true)), CREATETABLE) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], comment = "Hudi") + TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE) } val CompactionShowHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.CompactionShowHoodieTableCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], isInput = true) + val tableDesc = + TableDesc("table", classOf[CatalogTableTableExtractor], isInput = true, comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), SHOW_TBLPROPERTIES) } + val CompactionHoodiePathCommand = { + val cmd = "org.apache.spark.sql.hudi.command.CompactionHoodiePathCommand" + val uriDesc = UriDesc("path", classOf[StringURIExtractor], comment = "Hudi") + TableCommandSpec( + cmd, + Seq.empty, + CREATETABLE, + uriDescs = Seq(uriDesc)) + } + + val CompactionShowHoodiePathCommand = { + val cmd = "org.apache.spark.sql.hudi.command.CompactionShowHoodiePathCommand" + val uriDesc = UriDesc("path", classOf[StringURIExtractor], isInput = true, comment = "Hudi") + TableCommandSpec(cmd, Seq.empty, SHOW_TBLPROPERTIES, uriDescs = Seq(uriDesc)) + } + val CreateIndexCommand = { val cmd = "org.apache.spark.sql.hudi.command.CreateIndexCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor]) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), CREATEINDEX) } val DropIndexCommand = { val cmd = "org.apache.spark.sql.hudi.command.DropIndexCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor]) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), DROPINDEX) } val ShowIndexCommand = { val cmd = "org.apache.spark.sql.hudi.command.ShowIndexesCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], isInput = true) + val tableDesc = + TableDesc("table", classOf[CatalogTableTableExtractor], isInput = true, comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), SHOWINDEXES) } val RefreshIndexCommand = { val cmd = "org.apache.spark.sql.hudi.command.RefreshIndexCommand" - val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor]) + val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), ALTERINDEX_REBUILD) } @@ -175,52 +214,62 @@ object HudiCommands { "logicalRelation", classOf[LogicalRelationTableExtractor], actionTypeDesc = - Some(ActionTypeDesc("overwrite", classOf[OverwriteOrInsertActionTypeExtractor]))) + Some(ActionTypeDesc( + "overwrite", + classOf[OverwriteOrInsertActionTypeExtractor], + comment = "Hudi")), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(QueryDesc("query"))) } val ShowHoodieTablePartitionsCommand = { val cmd = "org.apache.spark.sql.hudi.command.ShowHoodieTablePartitionsCommand" - val columnDesc = ColumnDesc("specOpt", classOf[PartitionOptionColumnExtractor]) + val columnDesc = + ColumnDesc("specOpt", classOf[PartitionOptionColumnExtractor], comment = "Hudi") val tableDesc = TableDesc( "tableIdentifier", classOf[TableIdentifierTableExtractor], isInput = true, - columnDesc = Some(columnDesc)) + columnDesc = Some(columnDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), SHOWPARTITIONS) } val DeleteHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.DeleteHoodieTableCommand" - val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE)) + val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE), comment = "Hudi") val tableDesc = TableDesc( "dft", classOf[HudiDataSourceV2RelationTableExtractor], - actionTypeDesc = Some(actionTypeDesc)) + actionTypeDesc = Some(actionTypeDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc)) } val UpdateHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.UpdateHoodieTableCommand" - val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE)) + val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE), comment = "Hudi") val tableDesc = TableDesc( "ut", classOf[HudiDataSourceV2RelationTableExtractor], - actionTypeDesc = Some(actionTypeDesc)) + actionTypeDesc = Some(actionTypeDesc), + comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc)) } val MergeIntoHoodieTableCommand = { val cmd = "org.apache.spark.sql.hudi.command.MergeIntoHoodieTableCommand" - val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE)) + val actionTypeDesc = ActionTypeDesc(actionType = Some(UPDATE), comment = "Hudi") val tableDesc = TableDesc( "mergeInto", classOf[HudiMergeIntoTargetTableExtractor], - actionTypeDesc = Some(actionTypeDesc)) - val queryDescs = QueryDesc("mergeInto", classOf[HudiMergeIntoSourceTableExtractor]) + actionTypeDesc = Some(actionTypeDesc), + comment = "Hudi") + val queryDescs = + QueryDesc("mergeInto", classOf[HudiMergeIntoSourceTableExtractor], comment = "Hudi") TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryDescs)) } @@ -242,7 +291,7 @@ object HudiCommands { setCurrentDatabaseIfMissing = true))) } - val data: Array[TableCommandSpec] = Array( + override def specs: Seq[TableCommandSpec] = Seq( AlterHoodieTableAddColumnsCommand, AlterHoodieTableChangeColumnCommand, AlterHoodieTableDropPartitionCommand, @@ -253,7 +302,9 @@ object HudiCommands { CreateHoodieTableCommand, CreateHoodieTableLikeCommand, CreateIndexCommand, + CompactionHoodiePathCommand, CompactionHoodieTableCommand, + CompactionShowHoodiePathCommand, CompactionShowHoodieTableCommand, DeleteHoodieTableCommand, DropHoodieTableCommand, diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/IcebergCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/IcebergCommands.scala index fb195b4554c..33e94d718c2 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/IcebergCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/IcebergCommands.scala @@ -21,7 +21,7 @@ import org.apache.kyuubi.plugin.spark.authz.OperationType import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType._ import org.apache.kyuubi.plugin.spark.authz.serde._ -object IcebergCommands { +object IcebergCommands extends CommandSpecs[TableCommandSpec] { val DeleteFromIcebergTable = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.DeleteFromIcebergTable" @@ -30,7 +30,8 @@ object IcebergCommands { TableDesc( "table", classOf[DataSourceV2RelationTableExtractor], - actionTypeDesc = Some(actionTypeDesc)) + actionTypeDesc = Some(actionTypeDesc), + comment = "Iceberg") TableCommandSpec(cmd, Seq(tableDesc)) } @@ -45,18 +46,19 @@ object IcebergCommands { val tableDesc = TableDesc( "targetTable", classOf[DataSourceV2RelationTableExtractor], - actionTypeDesc = Some(actionTypeDesc)) + actionTypeDesc = Some(actionTypeDesc), + comment = "Iceberg") val queryDesc = QueryDesc("sourceTable") TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryDesc)) } val CallProcedure = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.Call" - val td = TableDesc("args", classOf[ExpressionSeqTableExtractor]) + val td = TableDesc("args", classOf[ExpressionSeqTableExtractor], comment = "Iceberg") TableCommandSpec(cmd, Seq(td), opType = OperationType.ALTERTABLE_PROPERTIES) } - val data: Array[TableCommandSpec] = Array( + override def specs: Seq[TableCommandSpec] = Seq( CallProcedure, DeleteFromIcebergTable, UpdateIcebergTable, diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala index 1b2d330d1cb..58d161ce051 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala @@ -24,7 +24,9 @@ import java.nio.file.{Files, Paths, StandardOpenOption} import org.scalatest.funsuite.AnyFunSuite import org.apache.kyuubi.plugin.spark.authz.serde.{mapper, CommandSpec} +import org.apache.kyuubi.plugin.spark.authz.serde.CommandSpecs import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ /** * Generates the default command specs to src/main/resources dir. @@ -42,26 +44,29 @@ import org.apache.kyuubi.util.AssertionUtils._ class JsonSpecFileGenerator extends AnyFunSuite { // scalastyle:on test("check spec json files") { - writeCommandSpecJson("database", DatabaseCommands.data) - writeCommandSpecJson("table", TableCommands.data ++ IcebergCommands.data ++ HudiCommands.data) - writeCommandSpecJson("function", FunctionCommands.data) - writeCommandSpecJson("scan", Scans.data) + writeCommandSpecJson("database", Seq(DatabaseCommands)) + writeCommandSpecJson("table", Seq(TableCommands, IcebergCommands, HudiCommands, DeltaCommands)) + writeCommandSpecJson("function", Seq(FunctionCommands)) + writeCommandSpecJson("scan", Seq(Scans)) } def writeCommandSpecJson[T <: CommandSpec]( commandType: String, - specArr: Array[T]): Unit = { - val pluginHome = getClass.getProtectionDomain.getCodeSource.getLocation.getPath - .split("target").head + specsArr: Seq[CommandSpecs[T]]): Unit = { val filename = s"${commandType}_command_spec.json" - val filePath = Paths.get(pluginHome, "src", "main", "resources", filename) + val filePath = Paths.get( + s"${getCurrentModuleHome(this)}/src/main/resources/$filename") - val generatedStr = mapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(specArr.sortBy(_.classname)) + val allSpecs = specsArr.flatMap(_.specs.sortBy(_.classname)) + val duplicatedClassnames = allSpecs.groupBy(_.classname).values + .filter(_.size > 1).flatMap(specs => specs.map(_.classname)).toSet + withClue(s"Unexpected duplicated classnames: $duplicatedClassnames")( + assertResult(0)(duplicatedClassnames.size)) + val generatedStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(allSpecs) if (sys.env.get("KYUUBI_UPDATE").contains("1")) { // scalastyle:off println - println(s"writing ${specArr.length} specs to $filename") + println(s"writing ${allSpecs.length} specs to $filename") // scalastyle:on println Files.write( filePath, diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/Scans.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/Scans.scala index b2c1868a26d..ed17dc5dc43 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/Scans.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/Scans.scala @@ -20,7 +20,7 @@ package org.apache.kyuubi.plugin.spark.authz.gen import org.apache.kyuubi.plugin.spark.authz.serde._ import org.apache.kyuubi.plugin.spark.authz.serde.FunctionType._ -object Scans { +object Scans extends CommandSpecs[ScanSpec] { val HiveTableRelation = { val r = "org.apache.spark.sql.catalyst.catalog.HiveTableRelation" @@ -37,7 +37,8 @@ object Scans { ScanDesc( "catalogTable", classOf[CatalogTableOptionTableExtractor]) - ScanSpec(r, Seq(tableDesc)) + val uriDesc = UriDesc("relation", classOf[BaseRelationFileIndexURIExtractor]) + ScanSpec(r, Seq(tableDesc), uriDescs = Seq(uriDesc)) } val DataSourceV2Relation = { @@ -50,7 +51,7 @@ object Scans { } val PermanentViewMarker = { - val r = "org.apache.kyuubi.plugin.spark.authz.util.PermanentViewMarker" + val r = "org.apache.kyuubi.plugin.spark.authz.rule.permanentview.PermanentViewMarker" val tableDesc = ScanDesc( "catalogTable", @@ -79,7 +80,7 @@ object Scans { val HiveGenericUDTF = HiveSimpleUDF.copy(classname = "org.apache.spark.sql.hive.HiveGenericUDTF") - val data: Array[ScanSpec] = Array( + override def specs: Seq[ScanSpec] = Seq( HiveTableRelation, LogicalRelation, DataSourceV2Relation, diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/TableCommands.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/TableCommands.scala index 9893953afb7..aced937b9a6 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/TableCommands.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/TableCommands.scala @@ -22,7 +22,7 @@ import org.apache.kyuubi.plugin.spark.authz.PrivilegeObjectActionType._ import org.apache.kyuubi.plugin.spark.authz.serde._ import org.apache.kyuubi.plugin.spark.authz.serde.TableType._ -object TableCommands { +object TableCommands extends CommandSpecs[TableCommandSpec] { // table extractors val tite = classOf[TableIdentifierTableExtractor] val tableNameDesc = TableDesc("tableName", tite) @@ -39,7 +39,8 @@ object TableCommands { val AlterTable = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.AlterTable" val tableDesc = TableDesc("ident", classOf[IdentifierTableExtractor]) - TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_PROPERTIES) + val uriDescs = Seq(UriDesc("ident", classOf[IdentifierURIExtractor])) + TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_PROPERTIES, uriDescs = uriDescs) } val AlterTableAddColumns = { @@ -51,7 +52,8 @@ object TableCommands { val AddColumns = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.AddColumns" - TableCommandSpec(cmd, Seq(resolvedTableDesc), ALTERTABLE_ADDCOLS) + val uriDescs = Seq(UriDesc("child", classOf[ResolvedTableURIExtractor])) + TableCommandSpec(cmd, Seq(resolvedTableDesc), ALTERTABLE_ADDCOLS, uriDescs = uriDescs) } val AlterColumn = { @@ -66,22 +68,24 @@ object TableCommands { val ReplaceColumns = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.ReplaceColumns" - TableCommandSpec(cmd, Seq(resolvedTableDesc), ALTERTABLE_REPLACECOLS) + AddColumns.copy(classname = cmd, opType = ALTERTABLE_REPLACECOLS) } val RenameColumn = { val cmd = "org.apache.spark.sql.catalyst.plans.logical.RenameColumn" - TableCommandSpec(cmd, Seq(resolvedTableDesc), ALTERTABLE_RENAMECOL) + AddColumns.copy(classname = cmd, opType = ALTERTABLE_RENAMECOL) } val AlterTableAddPartition = { val cmd = "org.apache.spark.sql.execution.command.AlterTableAddPartitionCommand" val columnDesc = ColumnDesc("partitionSpecsAndLocs", classOf[PartitionLocsSeqColumnExtractor]) + val uriDesc = UriDesc("partitionSpecsAndLocs", classOf[PartitionLocsSeqURIExtractor]) TableCommandSpec( cmd, Seq(tableNameDesc.copy(columnDesc = Some(columnDesc))), - ALTERTABLE_ADDPARTS) + ALTERTABLE_ADDPARTS, + uriDescs = Seq(uriDesc)) } val AlterTableChangeColumn = { @@ -150,10 +154,12 @@ object TableCommands { val AlterTableSetLocation = { val cmd = "org.apache.spark.sql.execution.command.AlterTableSetLocationCommand" val columnDesc = ColumnDesc("partitionSpec", classOf[PartitionOptionColumnExtractor]) + val uriDesc = UriDesc("location", classOf[StringURIExtractor]) TableCommandSpec( cmd, Seq(tableNameDesc.copy(columnDesc = Some(columnDesc))), - ALTERTABLE_LOCATION) + ALTERTABLE_LOCATION, + uriDescs = Seq(uriDesc)) } val AlterTableSetProperties = TableCommandSpec( @@ -210,10 +216,15 @@ object TableCommands { "tableName", classOf[IdentifierTableExtractor], catalogDesc = Some(CatalogDesc())) + val uriDescs = Seq( + UriDesc("tableSpec", classOf[TableSpecURIExtractor]), + UriDesc("properties", classOf[PropertiesLocationUriExtractor]), + UriDesc("tableName", classOf[IdentifierURIExtractor])) TableCommandSpec( cmd, Seq(resolvedIdentifierTableDesc, tableDesc, resolvedDbObjectNameDesc), - CREATETABLE) + CREATETABLE, + uriDescs = uriDescs) } val CreateV2Table = { @@ -222,7 +233,10 @@ object TableCommands { "tableName", classOf[IdentifierTableExtractor], catalogDesc = Some(CatalogDesc())) - TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE) + val uriDescs = Seq( + UriDesc("properties", classOf[PropertiesLocationUriExtractor]), + UriDesc("tableName", classOf[IdentifierURIExtractor])) + TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE, uriDescs = uriDescs) } val CreateTableAsSelectV2 = { @@ -231,6 +245,9 @@ object TableCommands { "tableName", classOf[IdentifierTableExtractor], catalogDesc = Some(CatalogDesc())) + val uriDescs = Seq( + UriDesc("tableSpec", classOf[TableSpecURIExtractor]), + UriDesc("properties", classOf[PropertiesLocationUriExtractor])) TableCommandSpec( cmd, Seq( @@ -238,7 +255,8 @@ object TableCommands { tableDesc, resolvedDbObjectNameDesc.copy(fieldName = "name")), CREATETABLE_AS_SELECT, - Seq(queryQueryDesc)) + Seq(queryQueryDesc), + uriDescs = uriDescs) } val CommentOnTable = { @@ -254,7 +272,8 @@ object TableCommands { "table", classOf[DataSourceV2RelationTableExtractor], actionTypeDesc = Some(actionTypeDesc)) - TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryQueryDesc)) + val uriDescs = Seq(UriDesc("table", classOf[DataSourceV2RelationURIExtractor])) + TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryQueryDesc), uriDescs = uriDescs) } val ReplaceData = { @@ -292,7 +311,8 @@ object TableCommands { "table", classOf[DataSourceV2RelationTableExtractor], actionTypeDesc = Some(actionTypeDesc)) - TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryQueryDesc)) + val uriDescs = Seq(UriDesc("table", classOf[DataSourceV2RelationURIExtractor])) + TableCommandSpec(cmd, Seq(tableDesc), queryDescs = Seq(queryQueryDesc), uriDescs = uriDescs) } val OverwritePartitionsDynamic = { @@ -372,14 +392,21 @@ object TableCommands { val cmd = "org.apache.spark.sql.execution.datasources.CreateTable" val tableDesc = TableDesc("tableDesc", classOf[CatalogTableTableExtractor]) val queryDesc = QueryDesc("query", "LogicalPlanOptionQueryExtractor") - TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE, queryDescs = Seq(queryDesc)) + val uriDesc = UriDesc("tableDesc", classOf[CatalogTableURIExtractor]) + TableCommandSpec( + cmd, + Seq(tableDesc), + CREATETABLE, + queryDescs = Seq(queryDesc), + uriDescs = Seq(uriDesc)) } val CreateDataSourceTable = { val cmd = "org.apache.spark.sql.execution.command.CreateDataSourceTableCommand" val tableDesc = TableDesc("table", classOf[CatalogTableTableExtractor], setCurrentDatabaseIfMissing = true) - TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE) + val uriDesc = UriDesc("table", classOf[CatalogTableURIExtractor]) + TableCommandSpec(cmd, Seq(tableDesc), CREATETABLE, uriDescs = Seq(uriDesc)) } val CreateDataSourceTableAsSelect = { @@ -395,8 +422,14 @@ object TableCommands { val columnDesc = ColumnDesc("outputColumnNames", classOf[StringSeqColumnExtractor]) val tableDesc = TableDesc("tableDesc", classOf[CatalogTableTableExtractor], Some(columnDesc)) + val uriDesc = UriDesc("tableDesc", classOf[CatalogTableURIExtractor]) val queryDesc = queryQueryDesc - TableCommandSpec(cmd, Seq(tableDesc), "CREATETABLE_AS_SELECT", queryDescs = Seq(queryDesc)) + TableCommandSpec( + cmd, + Seq(tableDesc), + "CREATETABLE_AS_SELECT", + queryDescs = Seq(queryDesc), + uriDescs = Seq(uriDesc)) } val CreateTableLike = { @@ -410,7 +443,8 @@ object TableCommands { classOf[TableIdentifierTableExtractor], isInput = true, setCurrentDatabaseIfMissing = true) - TableCommandSpec(cmd, Seq(tableDesc1, tableDesc2), CREATETABLE) + val uriDesc = UriDesc("fileFormat", classOf[CatalogStorageFormatURIExtractor]) + TableCommandSpec(cmd, Seq(tableDesc1, tableDesc2), CREATETABLE, uriDescs = Seq(uriDesc)) } val DescribeColumn = { @@ -552,7 +586,15 @@ object TableCommands { val InsertIntoDataSourceDir = { val cmd = "org.apache.spark.sql.execution.command.InsertIntoDataSourceDirCommand" val queryDesc = queryQueryDesc - TableCommandSpec(cmd, Nil, queryDescs = Seq(queryDesc)) + val uriDesc = UriDesc("storage", classOf[CatalogStorageFormatURIExtractor]) + TableCommandSpec(cmd, Nil, queryDescs = Seq(queryDesc), uriDescs = Seq(uriDesc)) + } + + val SaveIntoDataSourceCommand = { + val cmd = "org.apache.spark.sql.execution.datasources.SaveIntoDataSourceCommand" + val queryDesc = queryQueryDesc + val uriDesc = UriDesc("options", classOf[PropertiesPathUriExtractor]) + TableCommandSpec(cmd, Nil, queryDescs = Seq(queryDesc), uriDescs = Seq(uriDesc)) } val InsertIntoHadoopFsRelationCommand = { @@ -576,7 +618,8 @@ object TableCommands { fieldName = "table", columnDesc = Some(columnDesc), actionTypeDesc = Some(actionTypeDesc)) - TableCommandSpec(cmd, Seq(tableDesc), "LOAD") + val uriDesc = UriDesc("path", classOf[StringURIExtractor], isInput = true) + TableCommandSpec(cmd, Seq(tableDesc), LOAD, uriDescs = Seq(uriDesc)) } val RefreshTable = { @@ -594,7 +637,32 @@ object TableCommands { TableCommandSpec(cmd, Seq(tableIdentDesc.copy(isInput = true))) } - val data: Array[TableCommandSpec] = Array( + val SetTableProperties = { + val cmd = "org.apache.spark.sql.catalyst.plans.logical.SetTableProperties" + val tableDesc = TableDesc("table", classOf[ResolvedTableTableExtractor]) + val uriDescs = Seq(UriDesc("table", classOf[ResolvedTableURIExtractor])) + TableCommandSpec(cmd, Seq(tableDesc), ALTERTABLE_PROPERTIES, uriDescs = uriDescs) + } + + val AddArchivesCommand = { + val cmd = "org.apache.spark.sql.execution.command.AddArchivesCommand" + val uriDesc = UriDesc("paths", classOf[StringSeqURIExtractor], isInput = true) + TableCommandSpec(cmd, Nil, ADD, uriDescs = Seq(uriDesc)) + } + + // For spark-3.1 + val AddFileCommand = { + val cmd = "org.apache.spark.sql.execution.command.AddFileCommand" + val uriDesc = UriDesc("path", classOf[StringURIExtractor], isInput = true) + TableCommandSpec(cmd, Nil, ADD, uriDescs = Seq(uriDesc)) + } + + override def specs: Seq[TableCommandSpec] = Seq( + AddArchivesCommand, + AddArchivesCommand.copy(classname = "org.apache.spark.sql.execution.command.AddFilesCommand"), + AddArchivesCommand.copy(classname = "org.apache.spark.sql.execution.command.AddJarsCommand"), + AddFileCommand, + AddFileCommand.copy(classname = "org.apache.spark.sql.execution.command.AddJarCommand"), AddPartitions, DropPartitions, RenamePartitions, @@ -653,8 +721,7 @@ object TableCommands { DropTableV2, InsertIntoDataSource, InsertIntoDataSourceDir, - InsertIntoDataSourceDir.copy(classname = - "org.apache.spark.sql.execution.datasources.SaveIntoDataSourceCommand"), + SaveIntoDataSourceCommand, InsertIntoHadoopFsRelationCommand, InsertIntoDataSourceDir.copy(classname = "org.apache.spark.sql.hive.execution.InsertIntoHiveDirCommand"), @@ -668,6 +735,7 @@ object TableCommands { RefreshTableV2, RefreshTable3d0, ReplaceData, + SetTableProperties, ShowColumns, ShowCreateTable, ShowCreateTable.copy(classname = diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/DeltaCatalogRangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/DeltaCatalogRangerSparkExtensionSuite.scala new file mode 100644 index 00000000000..1ce8ad6765f --- /dev/null +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/DeltaCatalogRangerSparkExtensionSuite.scala @@ -0,0 +1,573 @@ +/* + * 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.kyuubi.plugin.spark.authz.ranger + +import java.nio.file.Path + +import org.scalatest.Outcome + +import org.apache.kyuubi.Utils +import org.apache.kyuubi.plugin.spark.authz.AccessControlException +import org.apache.kyuubi.plugin.spark.authz.RangerTestNamespace._ +import org.apache.kyuubi.plugin.spark.authz.RangerTestUsers._ +import org.apache.kyuubi.plugin.spark.authz.ranger.DeltaCatalogRangerSparkExtensionSuite._ +import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils.{isSparkV32OrGreater, isSparkV35OrGreater} +import org.apache.kyuubi.tags.DeltaTest +import org.apache.kyuubi.util.AssertionUtils._ + +/** + * Tests for RangerSparkExtensionSuite on Delta Lake + */ +@DeltaTest +class DeltaCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { + override protected val catalogImpl: String = "hive" + override protected val sqlExtensions: String = "io.delta.sql.DeltaSparkSessionExtension" + + val namespace1 = deltaNamespace + val table1 = "table1_delta" + val table2 = "table2_delta" + + def propString(props: Map[String, String]): String = + if (props.isEmpty) "" + else { + props + .map { case (key, value) => s"'$key' = '$value'" } + .mkString("TBLPROPERTIES (", ",", ")") + } + + def createTableSql(namespace: String, table: String): String = + s""" + |CREATE TABLE IF NOT EXISTS $namespace.$table ( + | id INT, + | name STRING, + | gender STRING, + | birthDate TIMESTAMP + |) + |USING DELTA + |PARTITIONED BY (gender) + |""".stripMargin + + def createPathBasedTableSql(path: Path, props: Map[String, String] = Map.empty): String = + s""" + |CREATE TABLE IF NOT EXISTS delta.`$path` ( + | id INT, + | name STRING, + | gender STRING, + | birthDate TIMESTAMP + |) + |USING DELTA + |PARTITIONED BY (gender) + |${propString(props)} + |""".stripMargin + + override def withFixture(test: NoArgTest): Outcome = { + test() + } + + override def beforeAll(): Unit = { + spark.conf.set(s"spark.sql.catalog.$sparkCatalog", deltaCatalogClassName) + spark.conf.set( + s"spark.sql.catalog.$sparkCatalog.warehouse", + Utils.createTempDir("delta-hadoop").toString) + super.beforeAll() + } + + override def afterAll(): Unit = { + super.afterAll() + spark.sessionState.catalog.reset() + spark.sessionState.conf.clear() + } + + test("create table") { + withCleanTmpResources(Seq( + (s"$namespace1.$table1", "table"), + (s"$namespace1.$table2", "table"), + (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + val createNonPartitionTableSql = + s""" + |CREATE TABLE IF NOT EXISTS $namespace1.$table1 ( + | id INT, + | name STRING, + | gender STRING, + | birthDate TIMESTAMP + |) USING DELTA + |""".stripMargin + interceptEndsWith[AccessControlException] { + doAs(someone, sql(createNonPartitionTableSql)) + }(s"does not have [create] privilege on [$namespace1/$table1]") + doAs(admin, sql(createNonPartitionTableSql)) + + val createPartitionTableSql = createTableSql(namespace1, table2) + interceptEndsWith[AccessControlException] { + doAs(someone, sql(createPartitionTableSql)) + }(s"does not have [create] privilege on [$namespace1/$table2]") + doAs(admin, sql(createPartitionTableSql)) + } + } + + test("create or replace table") { + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + val createOrReplaceTableSql = + s""" + |CREATE OR REPLACE TABLE $namespace1.$table1 ( + | id INT, + | name STRING, + | gender STRING, + | birthDate TIMESTAMP + |) USING DELTA + |""".stripMargin + interceptEndsWith[AccessControlException] { + doAs(someone, sql(createOrReplaceTableSql)) + }(s"does not have [create] privilege on [$namespace1/$table1]") + doAs(admin, sql(createOrReplaceTableSql)) + } + } + + test("alter table") { + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + + // add columns + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 ADD COLUMNS (age int)")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + + // change column + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql(s"ALTER TABLE $namespace1.$table1" + + s" CHANGE COLUMN gender gender STRING AFTER birthDate")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + + // replace columns + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql(s"ALTER TABLE $namespace1.$table1" + + s" REPLACE COLUMNS (id INT, name STRING)")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + + // rename column + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql(s"ALTER TABLE $namespace1.$table1" + + s" RENAME COLUMN birthDate TO dateOfBirth")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + + // drop column + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 DROP COLUMN birthDate")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + + // set properties + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql(s"ALTER TABLE $namespace1.$table1" + + s" SET TBLPROPERTIES ('delta.appendOnly' = 'true')")))( + s"does not have [alter] privilege on [$namespace1/$table1]") + } + } + + test("delete from table") { + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + val deleteFromTableSql = s"DELETE FROM $namespace1.$table1 WHERE birthDate < '1955-01-01'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(deleteFromTableSql)))( + s"does not have [update] privilege on [$namespace1/$table1]") + doAs(admin, sql(deleteFromTableSql)) + } + } + + test("insert table") { + withSingleCallEnabled { + withCleanTmpResources(Seq( + (s"$namespace1.$table1", "table"), + (s"$namespace1.$table2", "table"), + (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + doAs(admin, sql(createTableSql(namespace1, table2))) + + // insert into + val insertIntoSql = s"INSERT INTO $namespace1.$table1" + + s" SELECT * FROM $namespace1.$table2" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(insertIntoSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id,$namespace1/$table2/name," + + s"$namespace1/$table2/gender,$namespace1/$table2/birthDate]," + + s" [update] privilege on [$namespace1/$table1]") + doAs(admin, sql(insertIntoSql)) + + // insert overwrite + val insertOverwriteSql = s"INSERT OVERWRITE $namespace1.$table1" + + s" SELECT * FROM $namespace1.$table2" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(insertOverwriteSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id,$namespace1/$table2/name," + + s"$namespace1/$table2/gender,$namespace1/$table2/birthDate]," + + s" [update] privilege on [$namespace1/$table1]") + doAs(admin, sql(insertOverwriteSql)) + } + } + } + + test("update table") { + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + val updateTableSql = s"UPDATE $namespace1.$table1" + + s" SET gender = 'Female' WHERE gender = 'F'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(updateTableSql)))( + s"does not have [update] privilege on [$namespace1/$table1]") + doAs(admin, sql(updateTableSql)) + } + } + + test("merge into table") { + withSingleCallEnabled { + withCleanTmpResources(Seq( + (s"$namespace1.$table1", "table"), + (s"$namespace1.$table2", "table"), + (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + doAs(admin, sql(createTableSql(namespace1, table2))) + + val mergeIntoSql = + s""" + |MERGE INTO $namespace1.$table1 AS target + |USING $namespace1.$table2 AS source + |ON target.id = source.id + |WHEN MATCHED THEN + | UPDATE SET + | id = source.id, + | name = source.name, + | gender = source.gender, + | birthDate = source.birthDate + |WHEN NOT MATCHED + | THEN INSERT ( + | id, + | name, + | gender, + | birthDate + | ) + | VALUES ( + | source.id, + | source.name, + | source.gender, + | source.birthDate + | ) + |""".stripMargin + interceptEndsWith[AccessControlException]( + doAs(someone, sql(mergeIntoSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id,$namespace1/$table2/name," + + s"$namespace1/$table2/gender,$namespace1/$table2/birthDate]," + + s" [update] privilege on [$namespace1/$table1]") + doAs(admin, sql(mergeIntoSql)) + } + } + } + + test("optimize table") { + assume(isSparkV32OrGreater, "optimize table is available in Delta Lake 1.2.0 and above") + + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + val optimizeTableSql = s"OPTIMIZE $namespace1.$table1" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(optimizeTableSql)))( + s"does not have [alter] privilege on [$namespace1/$table1]") + doAs(admin, sql(optimizeTableSql)) + } + } + + test("vacuum table") { + withCleanTmpResources(Seq((s"$namespace1.$table1", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table1))) + val vacuumTableSql = s"VACUUM $namespace1.$table1" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(vacuumTableSql)))( + s"does not have [alter] privilege on [$namespace1/$table1]") + doAs(admin, sql(vacuumTableSql)) + } + } + + test("create path-based table") { + withTempDir(path => { + val createTableSql = createPathBasedTableSql(path) + interceptEndsWith[AccessControlException] { + doAs(someone, sql(createTableSql)) + }(s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(createTableSql)) + }) + } + + test("create or replace path-based table") { + withTempDir(path => { + val createOrReplaceTableSql = + s""" + |CREATE OR REPLACE TABLE delta.`$path` ( + | id INT, + | name STRING, + | gender STRING, + | birthDate TIMESTAMP + |) USING DELTA + |""".stripMargin + interceptEndsWith[AccessControlException] { + doAs(someone, sql(createOrReplaceTableSql)) + }(s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(createOrReplaceTableSql)) + }) + } + + test("delete from path-based table") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val deleteFromTableSql = s"DELETE FROM delta.`$path` WHERE birthDate < '1955-01-01'" + interceptEndsWith[AccessControlException] { + doAs(someone, sql(deleteFromTableSql)) + }(s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(deleteFromTableSql)) + }) + } + + test("update path-based table") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val updateTableSql = s"UPDATE delta.`$path` SET gender = 'Female' WHERE gender = 'F'" + interceptEndsWith[AccessControlException] { + doAs(someone, sql(updateTableSql)) + }(s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(updateTableSql)) + }) + } + + test("insert path-based table") { + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$namespace1.$table2", "table"), (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table2))) + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + // insert into + val insertIntoSql = s"INSERT INTO delta.`$path` SELECT * FROM $namespace1.$table2" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(insertIntoSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id," + + s"$namespace1/$table2/name,$namespace1/$table2/gender," + + s"$namespace1/$table2/birthDate], [write] privilege on [[$path, $path/]]") + doAs(admin, sql(insertIntoSql)) + + // insert overwrite + val insertOverwriteSql = + s"INSERT OVERWRITE delta.`$path` SELECT * FROM $namespace1.$table2" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(insertOverwriteSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id," + + s"$namespace1/$table2/name,$namespace1/$table2/gender," + + s"$namespace1/$table2/birthDate], [write] privilege on [[$path, $path/]]") + doAs(admin, sql(insertOverwriteSql)) + }) + } + } + } + + test("merge into path-based table") { + withSingleCallEnabled { + withCleanTmpResources(Seq( + (s"$namespace1.$table2", "table"), + (s"$namespace1", "database"))) { + doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) + doAs(admin, sql(createTableSql(namespace1, table2))) + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val mergeIntoSql = + s""" + |MERGE INTO delta.`$path` AS target + |USING $namespace1.$table2 AS source + |ON target.id = source.id + |WHEN MATCHED THEN + | UPDATE SET + | id = source.id, + | name = source.name, + | gender = source.gender, + | birthDate = source.birthDate + |WHEN NOT MATCHED + | THEN INSERT ( + | id, + | name, + | gender, + | birthDate + | ) + | VALUES ( + | source.id, + | source.name, + | source.gender, + | source.birthDate + | ) + |""".stripMargin + interceptEndsWith[AccessControlException]( + doAs(someone, sql(mergeIntoSql)))( + s"does not have [select] privilege on [$namespace1/$table2/id," + + s"$namespace1/$table2/name,$namespace1/$table2/gender," + + s"$namespace1/$table2/birthDate], [write] privilege on [[$path, $path/]]") + doAs(admin, sql(mergeIntoSql)) + }) + } + } + } + + test("optimize path-based table") { + assume(isSparkV32OrGreater, "optimize table is available in Delta Lake 1.2.0 and above") + + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val optimizeTableSql1 = s"OPTIMIZE delta.`$path`" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(optimizeTableSql1)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(optimizeTableSql1)) + + val optimizeTableSql2 = s"OPTIMIZE '$path'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(optimizeTableSql2)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(optimizeTableSql2)) + }) + } + + test("vacuum path-based table") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val vacuumTableSql1 = s"VACUUM delta.`$path`" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(vacuumTableSql1)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(vacuumTableSql1)) + + val vacuumTableSql2 = s"VACUUM '$path'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(vacuumTableSql2)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(vacuumTableSql2)) + }) + } + + test("alter path-based table set properties") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val setPropertiesSql = s"ALTER TABLE delta.`$path`" + + s" SET TBLPROPERTIES ('delta.appendOnly' = 'true')" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(setPropertiesSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(setPropertiesSql)) + }) + } + + test("alter path-based table add columns") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val addColumnsSql = s"ALTER TABLE delta.`$path` ADD COLUMNS (age int)" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(addColumnsSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(addColumnsSql)) + }) + } + + test("alter path-based table change column") { + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path))) + val changeColumnSql = s"ALTER TABLE delta.`$path`" + + s" CHANGE COLUMN gender gender STRING AFTER birthDate" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(changeColumnSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(changeColumnSql)) + }) + } + + test("alter path-based table drop column") { + assume( + isSparkV32OrGreater, + "alter table drop column is available in Delta Lake 1.2.0 and above") + + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path, Map("delta.columnMapping.mode" -> "name")))) + val dropColumnSql = s"ALTER TABLE delta.`$path` DROP COLUMN birthDate" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(dropColumnSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(dropColumnSql)) + }) + } + + test("alter path-based table rename column") { + assume( + isSparkV32OrGreater, + "alter table rename column is available in Delta Lake 1.2.0 and above") + + withTempDir(path => { + doAs(admin, sql(createPathBasedTableSql(path, Map("delta.columnMapping.mode" -> "name")))) + val renameColumnSql = s"ALTER TABLE delta.`$path`" + + s" RENAME COLUMN birthDate TO dateOfBirth" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(renameColumnSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + doAs(admin, sql(renameColumnSql)) + }) + } + + test("alter path-based table replace columns") { + withTempDir(path => { + assume( + isSparkV32OrGreater, + "alter table replace columns is not available in Delta Lake 1.0.1") + + doAs(admin, sql(createPathBasedTableSql(path, Map("delta.columnMapping.mode" -> "name")))) + val replaceColumnsSql = s"ALTER TABLE delta.`$path`" + + s" REPLACE COLUMNS (id INT, name STRING, gender STRING)" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(replaceColumnsSql)))( + s"does not have [write] privilege on [[$path, $path/]]") + + // There was a bug before Delta Lake 3.0, it will throw AnalysisException message + // "Cannot drop column from a struct type with a single field: + // StructType(StructField(birthDate,TimestampType,true))". + // For details, see https://github.com/delta-io/delta/pull/1822 + if (isSparkV35OrGreater) { + doAs(admin, sql(replaceColumnsSql)) + } + }) + } +} + +object DeltaCatalogRangerSparkExtensionSuite { + val deltaCatalogClassName: String = "org.apache.spark.sql.delta.catalog.DeltaCatalog" +} diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/HudiCatalogRangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/HudiCatalogRangerSparkExtensionSuite.scala index 04207291098..b6b9b6f31a5 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/HudiCatalogRangerSparkExtensionSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/HudiCatalogRangerSparkExtensionSuite.scala @@ -25,7 +25,7 @@ import org.apache.kyuubi.plugin.spark.authz.RangerTestNamespace._ import org.apache.kyuubi.plugin.spark.authz.RangerTestUsers._ import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ import org.apache.kyuubi.tags.HudiTest -import org.apache.kyuubi.util.AssertionUtils.interceptContains +import org.apache.kyuubi.util.AssertionUtils.interceptEndsWith /** * Tests for RangerSparkExtensionSuite on Hudi SQL. @@ -101,24 +101,24 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) // AlterHoodieTableAddColumnsCommand - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 ADD COLUMNS(age int)")))( s"does not have [alter] privilege on [$namespace1/$table1/age]") // AlterHoodieTableChangeColumnCommand - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 CHANGE COLUMN id id bigint")))( s"does not have [alter] privilege" + s" on [$namespace1/$table1/id]") // AlterHoodieTableDropPartitionCommand - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 DROP PARTITION (city='test')")))( s"does not have [alter] privilege" + s" on [$namespace1/$table1/city]") // AlterHoodieTableRenameCommand - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 RENAME TO $namespace1.$table2")))( s"does not have [alter] privilege" + s" on [$namespace1/$table1]") @@ -126,7 +126,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { // AlterTableCommand && Spark31AlterTableCommand try { sql("set hoodie.schema.on.read.enable=true") - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, sql(s"ALTER TABLE $namespace1.$table1 ADD COLUMNS(age int)")))( s"does not have [alter] privilege on [$namespace1/$table1]") } finally { @@ -138,7 +138,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { test("CreateHoodieTableCommand") { withCleanTmpResources(Seq((namespace1, "database"))) { doAs(admin, sql(s"CREATE DATABASE IF NOT EXISTS $namespace1")) - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql( @@ -171,7 +171,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |) |PARTITIONED BY(city) |""".stripMargin)) - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql( @@ -210,7 +210,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |LIKE $namespace1.$table1 |USING HUDI |""".stripMargin - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs( someone, sql( @@ -238,7 +238,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val dropTableSql = s"DROP TABLE IF EXISTS $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(dropTableSql)) }(s"does not have [drop] privilege on [$namespace1/$table1]") doAs(admin, sql(dropTableSql)) @@ -263,7 +263,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val repairTableSql = s"MSCK REPAIR TABLE $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(repairTableSql)) }(s"does not have [alter] privilege on [$namespace1/$table1]") doAs(admin, sql(repairTableSql)) @@ -288,7 +288,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val truncateTableSql = s"TRUNCATE TABLE $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(truncateTableSql)) }(s"does not have [update] privilege on [$namespace1/$table1]") doAs(admin, sql(truncateTableSql)) @@ -313,19 +313,58 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val compactionTable = s"RUN COMPACTION ON $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(compactionTable)) - }(s"does not have [select] privilege on [$namespace1/$table1]") + }(s"does not have [create] privilege on [$namespace1/$table1]") doAs(admin, sql(compactionTable)) val showCompactionTable = s"SHOW COMPACTION ON $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(showCompactionTable)) }(s"does not have [select] privilege on [$namespace1/$table1]") doAs(admin, sql(showCompactionTable)) } } + test("CompactionHoodiePathCommand / CompactionShowHoodiePathCommand") { + withSingleCallEnabled { + withCleanTmpResources(Seq.empty) { + val path1 = "hdfs://demo/test/hudi/path" + val compactOnPath = s"RUN COMPACTION ON '$path1'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(compactOnPath)))( + s"does not have [write] privilege on [[$path1, $path1/]]") + + val showCompactOnPath = s"SHOW COMPACTION ON '$path1'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(showCompactOnPath)))( + s"does not have [read] privilege on [[$path1, $path1/]]") + + val path2 = "file:///demo/test/hudi/path" + val compactOnPath2 = s"RUN COMPACTION ON '$path2'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(compactOnPath2)))( + s"does not have [write] privilege on [[$path2, $path2/]]") + + val showCompactOnPath2 = s"SHOW COMPACTION ON '$path2'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(showCompactOnPath2)))( + s"does not have [read] privilege on [[$path2, $path2/]]") + + val path3 = "hdfs://demo/test/hudi/path" + val compactOnPath3 = s"RUN COMPACTION ON '$path3'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(compactOnPath3)))( + s"does not have [write] privilege on [[$path3, $path3/]]") + + val showCompactOnPath3 = s"SHOW COMPACTION ON '$path3/'" + interceptEndsWith[AccessControlException]( + doAs(someone, sql(showCompactOnPath3)))( + s"does not have [read] privilege on [[$path3, $path3/]]") + } + } + } + test("InsertIntoHoodieTableCommand") { withSingleCallEnabled { withCleanTmpResources(Seq( @@ -363,7 +402,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |FROM $namespace1.$table2 |WHERE city = 'hangzhou' |""".stripMargin - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(insertIntoHoodieTableSql)) }(s"does not have [select] privilege on " + s"[$namespace1/$table2/id,$namespace1/$table2/name,hudi_ns/$table2/city], " + @@ -394,14 +433,14 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val showPartitionsSql = s"SHOW PARTITIONS $namespace1.$table1" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(showPartitionsSql)) }(s"does not have [select] privilege on [$namespace1/$table1]") doAs(admin, sql(showPartitionsSql)) val showPartitionSpecSql = s"SHOW PARTITIONS $namespace1.$table1 PARTITION (city = 'hangzhou')" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(showPartitionSpecSql)) }(s"does not have [select] privilege on [$namespace1/$table1/city]") doAs(admin, sql(showPartitionSpecSql)) @@ -445,13 +484,13 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |""".stripMargin)) val deleteFrom = s"DELETE FROM $namespace1.$table1 WHERE id = 10" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(deleteFrom)) }(s"does not have [update] privilege on [$namespace1/$table1]") doAs(admin, sql(deleteFrom)) val updateSql = s"UPDATE $namespace1.$table1 SET name = 'test' WHERE id > 10" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(updateSql)) }(s"does not have [update] privilege on [$namespace1/$table1]") doAs(admin, sql(updateSql)) @@ -465,10 +504,11 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |AND target.name == 'test' | THEN UPDATE SET id = source.id, name = source.name, city = source.city |""".stripMargin - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(mergeIntoSQL)) }(s"does not have [select] privilege on " + - s"[$namespace1/$table2/id,$namespace1/$table2/name,$namespace1/$table2/city]") + s"[$namespace1/$table2/id,$namespace1/$table2/name,$namespace1/$table2/city], " + + s"[update] privilege on [$namespace1/$table1]") doAs(admin, sql(mergeIntoSQL)) } } @@ -510,13 +550,14 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { val copy_to_table = s"CALL copy_to_table(table => '$namespace1.$table1', new_table => '$namespace1.$table2')" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(copy_to_table)) - }(s"does not have [select] privilege on [$namespace1/$table1]") + }(s"does not have [select] privilege on [$namespace1/$table1], " + + s"[update] privilege on [$namespace1/$table2]") doAs(admin, sql(copy_to_table)) val show_table_properties = s"CALL show_table_properties(table => '$namespace1.$table1')" - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(show_table_properties)) }(s"does not have [select] privilege on [$namespace1/$table1]") doAs(admin, sql(show_table_properties)) @@ -546,7 +587,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { // CreateIndexCommand val createIndex = s"CREATE INDEX $index1 ON $namespace1.$table1 USING LUCENE (id)" - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql(createIndex)))(s"does not have [index] privilege on [$namespace1/$table1]") @@ -554,7 +595,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { // RefreshIndexCommand val refreshIndex = s"REFRESH INDEX $index1 ON $namespace1.$table1" - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql(refreshIndex)))(s"does not have [alter] privilege on [$namespace1/$table1]") @@ -562,7 +603,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { // ShowIndexesCommand val showIndex = s"SHOW INDEXES FROM TABLE $namespace1.$table1" - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql(showIndex)))(s"does not have [select] privilege on [$namespace1/$table1]") @@ -570,7 +611,7 @@ class HudiCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { // DropIndexCommand val dropIndex = s"DROP INDEX $index1 ON $namespace1.$table1" - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs( someone, sql(dropIndex)))(s"does not have [drop] privilege on [$namespace1/$table1]") diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/IcebergCatalogRangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/IcebergCatalogRangerSparkExtensionSuite.scala index 28e13aff3c0..677b3945dda 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/IcebergCatalogRangerSparkExtensionSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/IcebergCatalogRangerSparkExtensionSuite.scala @@ -111,7 +111,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite s" on [$namespace1/$table1/id]")) withSingleCallEnabled { - interceptContains[AccessControlException](doAs(someone, sql(mergeIntoSql)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(mergeIntoSql)))( if (isSparkV35OrGreater) { s"does not have [select] privilege on [$namespace1/table1/id" + s",$namespace1/$table1/name,$namespace1/$table1/city]" @@ -121,7 +121,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite s" [update] privilege on [$bobNamespace/$bobSelectTable]" }) - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(bob, sql(mergeIntoSql)) }(s"does not have [update] privilege on [$bobNamespace/$bobSelectTable]") } @@ -131,7 +131,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite test("[KYUUBI #3515] UPDATE TABLE") { // UpdateTable - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(s"UPDATE $catalogV2.$namespace1.$table1 SET city='Guangzhou' WHERE id=1")) }(if (isSparkV35OrGreater) { s"does not have [select] privilege on [$namespace1/$table1/id]" @@ -147,7 +147,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite test("[KYUUBI #3515] DELETE FROM TABLE") { // DeleteFromTable - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(s"DELETE FROM $catalogV2.$namespace1.$table1 WHERE id=2")) }(if (isSparkV34OrGreater) { s"does not have [select] privilege on [$namespace1/$table1/id]" @@ -155,7 +155,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite s"does not have [update] privilege on [$namespace1/$table1]" }) - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(bob, sql(s"DELETE FROM $catalogV2.$bobNamespace.$bobSelectTable WHERE id=2")) }(s"does not have [update] privilege on [$bobNamespace/$bobSelectTable]") @@ -264,9 +264,9 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite .foreach(i => sql(s"INSERT INTO $table VALUES ($i, 'user_$i')")) }) - interceptContains[AccessControlException](doAs(someone, sql(rewriteDataFiles1)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(rewriteDataFiles1)))( s"does not have [alter] privilege on [$namespace1/$tableName]") - interceptContains[AccessControlException](doAs(someone, sql(rewriteDataFiles2)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(rewriteDataFiles2)))( s"does not have [alter] privilege on [$namespace1/$tableName]") /** @@ -326,7 +326,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite val callRollbackToSnapshot = s"CALL $catalogV2.system.rollback_to_snapshot (table => '$table', snapshot_id => $targetSnapshotId)" - interceptContains[AccessControlException](doAs(someone, sql(callRollbackToSnapshot)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(callRollbackToSnapshot)))( s"does not have [alter] privilege on [$namespace1/$tableName]") doAs(admin, sql(callRollbackToSnapshot)) } @@ -344,7 +344,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite s"CALL $catalogV2.system.rollback_to_timestamp (table => '$table', timestamp => TIMESTAMP '$targetTimestamp')" } - interceptContains[AccessControlException](doAs(someone, sql(callRollbackToTimestamp)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(callRollbackToTimestamp)))( s"does not have [alter] privilege on [$namespace1/$tableName]") doAs(admin, sql(callRollbackToTimestamp)) } @@ -359,7 +359,7 @@ class IcebergCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite val callSetCurrentSnapshot = s"CALL $catalogV2.system.set_current_snapshot (table => '$table', snapshot_id => $targetSnapshotId)" - interceptContains[AccessControlException](doAs(someone, sql(callSetCurrentSnapshot)))( + interceptEndsWith[AccessControlException](doAs(someone, sql(callSetCurrentSnapshot)))( s"does not have [alter] privilege on [$namespace1/$tableName]") doAs(admin, sql(callSetCurrentSnapshot)) } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/PaimonCatalogRangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/PaimonCatalogRangerSparkExtensionSuite.scala index 62cd9d62732..1ea039ec1e1 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/PaimonCatalogRangerSparkExtensionSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/PaimonCatalogRangerSparkExtensionSuite.scala @@ -76,7 +76,7 @@ class PaimonCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |) |""".stripMargin - interceptContains[AccessControlException] { + interceptEndsWith[AccessControlException] { doAs(someone, sql(createTable)) }(s"does not have [create] privilege on [$namespace1/$table1]") doAs(admin, createTable) diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala index c2e886f0246..9dd9613d8f9 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtensionSuite.scala @@ -17,10 +17,13 @@ package org.apache.kyuubi.plugin.spark.authz.ranger +import java.lang.reflect.UndeclaredThrowableException +import java.nio.file.Path + import scala.util.Try import org.apache.hadoop.security.UserGroupInformation -import org.apache.spark.sql.SparkSessionExtensions +import org.apache.spark.sql.{Row, SparkSessionExtensions} import org.apache.spark.sql.catalyst.analysis.NoSuchTableException import org.apache.spark.sql.catalyst.catalog.HiveTableRelation import org.apache.spark.sql.catalyst.plans.logical.Statistics @@ -30,10 +33,11 @@ import org.scalatest.BeforeAndAfterAll // scalastyle:off import org.scalatest.funsuite.AnyFunSuite +import org.apache.kyuubi.Utils import org.apache.kyuubi.plugin.spark.authz.{AccessControlException, SparkSessionProvider} import org.apache.kyuubi.plugin.spark.authz.RangerTestNamespace._ import org.apache.kyuubi.plugin.spark.authz.RangerTestUsers._ -import org.apache.kyuubi.plugin.spark.authz.ranger.RuleAuthorization.KYUUBI_AUTHZ_TAG +import org.apache.kyuubi.plugin.spark.authz.rule.Authorization.KYUUBI_AUTHZ_TAG import org.apache.kyuubi.plugin.spark.authz.util.AuthZUtils._ import org.apache.kyuubi.util.AssertionUtils._ import org.apache.kyuubi.util.reflect.ReflectUtils._ @@ -90,6 +94,14 @@ abstract class RangerSparkExtensionSuite extends AnyFunSuite } } + protected def withTempDir(f: Path => Unit): Unit = { + val dir = Utils.createTempDir() + try f(dir) + finally { + Utils.deleteDirectoryRecursively(dir.toFile) + } + } + /** * Enables authorizing in single call mode, * and disables authorizing in single call mode after calling `f` @@ -113,12 +125,12 @@ abstract class RangerSparkExtensionSuite extends AnyFunSuite if (i == 1) { assert(logicalPlan.getTagValue(KYUUBI_AUTHZ_TAG).isEmpty) } else { - assert(logicalPlan.getTagValue(KYUUBI_AUTHZ_TAG).getOrElse(false)) + assert(logicalPlan.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty) } rule.apply(logicalPlan) } - assert(logicalPlan.getTagValue(KYUUBI_AUTHZ_TAG).getOrElse(false)) + assert(logicalPlan.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty) } test("[KYUUBI #3226]: Another session should also check even if the plan is cached.") { @@ -140,7 +152,7 @@ abstract class RangerSparkExtensionSuite extends AnyFunSuite // session1: first query, should auth once.[LogicalRelation] val df = sql(select) val plan1 = df.queryExecution.optimizedPlan - assert(plan1.getTagValue(KYUUBI_AUTHZ_TAG).getOrElse(false)) + assert(plan1.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty) // cache df.cache() @@ -148,7 +160,7 @@ abstract class RangerSparkExtensionSuite extends AnyFunSuite // session1: second query, should auth once.[InMemoryRelation] // (don't need to check in again, but it's okay to check in once) val plan2 = sql(select).queryExecution.optimizedPlan - assert(plan1 != plan2 && plan2.getTagValue(KYUUBI_AUTHZ_TAG).getOrElse(false)) + assert(plan1 != plan2 && plan2.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty) // session2: should auth once. val otherSessionDf = spark.newSession().sql(select) @@ -159,7 +171,7 @@ abstract class RangerSparkExtensionSuite extends AnyFunSuite // make sure it use cache. assert(plan3.isInstanceOf[InMemoryRelation]) // auth once only. - assert(plan3.getTagValue(KYUUBI_AUTHZ_TAG).getOrElse(false)) + assert(plan3.getTagValue(KYUUBI_AUTHZ_TAG).nonEmpty) }) } } @@ -877,7 +889,7 @@ class HiveCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { sql(s"SELECT id as new_id, name, max_scope FROM $db1.$view1".stripMargin).show())) assert(e2.getMessage.contains( s"does not have [select] privilege on " + - s"[$db1/$view1/id,$db1/$view1/name,$db1/$view1/max_scope,$db1/$view1/sum_age]")) + s"[$db1/$view1/id,$db1/$view1/name,$db1/$view1/max_scope]")) } } } @@ -889,7 +901,7 @@ class HiveCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { val df = doAs( admin, sql(s"SELECT * FROM VALUES(1, 100),(2, 200),(3, 300) AS t(id, scope)")).persist() - interceptContains[AccessControlException]( + interceptEndsWith[AccessControlException]( doAs(someone, df.write.mode("overwrite").saveAsTable(table1)))( s"does not have [create] privilege on [$defaultDb/$table1]") } @@ -913,31 +925,550 @@ class HiveCatalogRangerSparkExtensionSuite extends RangerSparkExtensionSuite { |CREATE VIEW $db1.$view2 |AS |SELECT count(*) as cnt, sum(id) as sum_id FROM $db1.$table1 - """.stripMargin)) - val e1 = intercept[AccessControlException]( - doAs(someone, sql(s"SELECT count(*) FROM $db1.$table1").show())) - assert(e1.getMessage.contains( - s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope]")) + """.stripMargin)) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(*) FROM $db1.$table1").show()))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(*) FROM $db1.$view1").show()))( + s"does not have [select] privilege on [$db1/$view1/id,$db1/$view1/scope]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(*) FROM $db1.$view2").show()))( + s"does not have [select] privilege on [$db1/$view2/cnt,$db1/$view2/sum_id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(id) FROM $db1.$table1 WHERE id > 10").show()))( + s"does not have [select] privilege on [$db1/$table1/id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(id) FROM $db1.$view1 WHERE id > 10").show()))( + s"does not have [select] privilege on [$db1/$view1/id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(sum_id) FROM $db1.$view2 WHERE sum_id > 10").show()))( + s"does not have [select] privilege on [$db1/$view2/sum_id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(scope) FROM $db1.$table1 WHERE id > 10").show()))( + s"does not have [select] privilege on [$db1/$table1/scope,$db1/$table1/id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(scope) FROM $db1.$view1 WHERE id > 10").show()))( + s"does not have [select] privilege on [$db1/$view1/scope,$db1/$view1/id]") + + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT count(cnt) FROM $db1.$view2 WHERE sum_id > 10").show()))( + s"does not have [select] privilege on [$db1/$view2/cnt,$db1/$view2/sum_id]") + } + } + } - val e2 = intercept[AccessControlException]( - doAs(someone, sql(s"SELECT count(*) FROM $db1.$view1").show())) - assert(e2.getMessage.contains( - s"does not have [select] privilege on [$db1/$view1/id,$db1/$view1/scope]")) - - val e3 = intercept[AccessControlException]( - doAs(someone, sql(s"SELECT count(*) FROM $db1.$view2").show())) - assert(e3.getMessage.contains( - s"does not have [select] privilege on [$db1/$view2/cnt,$db1/$view2/sum_id]")) - - val e4 = intercept[AccessControlException]( - doAs(someone, sql(s"SELECT count(*) FROM $db1.$view2 WHERE cnt > 10").show())) - assert(e4.getMessage.contains( - s"does not have [select] privilege on [$db1/$view2/cnt,$db1/$view2/sum_id]")) - - val e5 = intercept[AccessControlException]( - doAs(someone, sql(s"SELECT count(cnt) FROM $db1.$view2 WHERE cnt > 10").show())) - assert(e5.getMessage.contains( - s"does not have [select] privilege on [$db1/$view2/cnt,$db1/$view2/sum_id]")) + test("[KYUUBI #5503][AUTHZ] Check plan auth checked should not set tag to all child nodes") { + assume(isSparkV32OrGreater, "Spark 3.1 not support lateral subquery.") + val db1 = defaultDb + val table1 = "table1" + val table2 = "table2" + val perm_view = "perm_view" + withSingleCallEnabled { + withCleanTmpResources( + Seq( + (s"$db1.$table1", "table"), + (s"$db1.$table2", "table"), + (s"$db1.$perm_view", "view"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table2 (id int, scope int)")) + doAs(admin, sql(s"CREATE VIEW $db1.$perm_view AS SELECT * FROM $db1.$table2")) + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |SELECT t1.id + |FROM $db1.$table1 t1, + |LATERAL ( + | SELECT * + | FROM $db1.$perm_view t2 + | WHERE t1.id = t2.id + |) + |""".stripMargin).show()))( + s"does not have [select] privilege on " + + s"[$db1/$perm_view/id,$db1/$perm_view/scope]") + interceptEndsWith[AccessControlException]( + doAs( + permViewOnlyUser, + sql( + s""" + |SELECT t1.id + |FROM $db1.$table1 t1, + |LATERAL ( + | SELECT * + | FROM $db1.$perm_view t2 + | WHERE t1.id = t2.id + |) + |""".stripMargin).show()))( + s"does not have [select] privilege on " + + s"[$db1/$table1/id]") + + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |SELECT t1.id + |FROM $db1.$table1 t1, + |LATERAL ( + | SELECT * + | FROM $db1.$table2 t2 + | WHERE t1.id = t2.id + |) + |""".stripMargin).show()))( + s"does not have [select] privilege on " + + s"[$db1/$table2/id,$db1/$table2/scope]") + interceptEndsWith[AccessControlException]( + doAs( + table2OnlyUser, + sql( + s""" + |SELECT t1.id + |FROM $db1.$table1 t1, + |LATERAL ( + | SELECT * + | FROM $db1.$table2 t2 + | WHERE t1.id = t2.id + |) + |""".stripMargin).show()))( + s"does not have [select] privilege on " + + s"[$db1/$table1/id]") + } + } + } + + test("InsertIntoHiveDirCommand") { + val db1 = defaultDb + val table1 = "table1" + withTempDir { path => + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + interceptEndsWith[AccessControlException](doAs( + someone, + sql( + s""" + |INSERT OVERWRITE DIRECTORY '$path' + |ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' + |SELECT * FROM $db1.$table1""".stripMargin)))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope], " + + s"[write] privilege on [[$path, $path/]]") + } + } + } + } + + test("InsertIntoDataSourceDirCommand") { + val db1 = defaultDb + val table1 = "table1" + withTempDir { path => + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + interceptEndsWith[AccessControlException](doAs( + someone, + sql( + s""" + |INSERT OVERWRITE DIRECTORY '$path' + |USING parquet + |SELECT * FROM $db1.$table1""".stripMargin)))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope], " + + s"[write] privilege on [[$path, $path/]]") + } + } + } + } + + test("SaveIntoDataSourceCommand") { + withTempDir { path => + withSingleCallEnabled { + val df = sql("SELECT 1 as id, 'Tony' as name") + interceptEndsWith[AccessControlException](doAs( + someone, + df.write.format("console").mode("append").save(path.toString)))( + s"does not have [write] privilege on [[$path, $path/]]") + } + } + } + + test("HadoopFsRelation") { + val db1 = defaultDb + val table1 = "table1" + withTempDir { path => + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + doAs( + admin, + sql( + s""" + |INSERT OVERWRITE DIRECTORY '$path' + |USING parquet + |SELECT * FROM $db1.$table1""".stripMargin)) + + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |INSERT OVERWRITE DIRECTORY '$path' + |USING parquet + |SELECT * FROM $db1.$table1""".stripMargin)))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope], " + + s"[write] privilege on [[$path, $path/]]") + + doAs(admin, sql(s"SELECT * FROM parquet.`$path`".stripMargin).explain(true)) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT * FROM parquet.`$path`".stripMargin).explain(true)))( + s"does not have [read] privilege on " + + s"[[file:$path, file:$path/]]") + } + } + } + } + + test("LoadDataCommand") { + val db1 = defaultDb + val table1 = "table1" + withSingleCallEnabled { + withTempDir { path => + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + val loadDataSql = + s""" + |LOAD DATA LOCAL INPATH '$path' + |OVERWRITE INTO TABLE $db1.$table1 + |""".stripMargin + doAs(admin, sql(loadDataSql).explain(true)) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(loadDataSql).explain(true)))( + s"does not have [read] privilege on [[$path, $path/]], " + + s"[update] privilege on [$db1/$table1]") + } + } + } + } + + test("Add resource command") { + withTempDir { path => + withSingleCallEnabled { + val supportedCommand = if (isSparkV32OrGreater) { + Seq("JAR", "FILE", "ARCHIVE") + } else { + Seq("JAR", "FILE") + } + supportedCommand.foreach { cmd => + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"ADD $cmd $path")))( + s"does not have [read] privilege on [[$path, $path/]]") + } + } + } + } + + test("CreateDatabaseCommand/AlterDatabaseSetLocationCommand") { + val db1 = "db1" + withSingleCallEnabled { + withTempDir { path1 => + withTempDir { path2 => + withCleanTmpResources(Seq((s"$db1", "database"))) { + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"CREATE DATABASE $db1 LOCATION '$path1'")))( + s"does not have [create] privilege on [$db1], " + + s"[write] privilege on [[$path1, $path1/]]") + doAs(admin, sql(s"CREATE DATABASE $db1 LOCATION '$path1'")) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"ALTER DATABASE $db1 SET LOCATION '$path2'")))( + s"does not have [alter] privilege on [$db1], " + + s"[write] privilege on [[$path2, $path2/]]") + val e = intercept[UndeclaredThrowableException]( + doAs(admin, sql(s"ALTER DATABASE $db1 SET LOCATION '$path2'"))) + assert(e.getCause.getMessage.contains("does not support altering database location")) + } + } + } + } + } + + test("AlterTableSetLocationCommand/AlterTableAddPartitionCommand") { + val db1 = defaultDb + val table1 = "table1" + val table2 = "table2" + withSingleCallEnabled { + withTempDir { path1 => + withCleanTmpResources(Seq((s"$db1.$table1", "table"), (s"$db1.$table2", "table"))) { + doAs( + admin, + sql( + s""" + |CREATE TABLE IF NOT EXISTS $db1.$table1( + |id int, + |scope int, + |day string) + |PARTITIONED BY (day) + |""".stripMargin)) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"ALTER TABLE $db1.$table1 SET LOCATION '$path1'")))( + s"does not have [alter] privilege on [$db1/$table1], " + + s"[write] privilege on [[$path1, $path1/]]") + + withTempDir { path2 => + withTempDir { path3 => + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |ALTER TABLE $db1.$table1 + |ADD + |PARTITION (day='2023-01-01') LOCATION '$path2' + |PARTITION (day='2023-01-02') LOCATION '$path3' + |""".stripMargin)))( + s"does not have [alter] privilege on [$db1/$table1/day], " + + s"[write] privilege on [[$path2, $path2/],[$path3, $path3/]]") + } + } + } + } + } + } + + test("Table Command location privilege") { + val db1 = defaultDb + val table1 = "table1" + val table2 = "table2" + withSingleCallEnabled { + withTempDir { path => + withCleanTmpResources(Seq((s"$db1.$table1", "table"), (s"$db1.$table2", "table"))) { + interceptEndsWith[AccessControlException](doAs( + someone, + sql( + s""" + |CREATE TABLE IF NOT EXISTS $db1.$table1(id int, scope int) + |LOCATION '$path'""".stripMargin)))( + if (!isSparkV35OrGreater) { + s"does not have [create] privilege on [$db1/$table1], " + + s"[write] privilege on [[$path, $path/]]" + } else { + s"does not have [create] privilege on [$db1/$table1], " + + s"[write] privilege on [[file://$path, file://$path/]]" + }) + doAs( + admin, + sql( + s""" + |CREATE TABLE IF NOT EXISTS $db1.$table1(id int, scope int) + |LOCATION '$path'""".stripMargin)) + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |CREATE TABLE $db1.$table2 + |LIKE $db1.$table1 + |LOCATION '$path' + |""".stripMargin)))( + s"does not have [select] privilege on [$db1/$table1], " + + s"[create] privilege on [$db1/$table2], " + + s"[write] privilege on [[$path, $path/]]") + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |CREATE TABLE $db1.$table2 + |LOCATION '$path' + |AS + |SELECT * FROM $db1.$table1 + |""".stripMargin)))( + if (!isSparkV35OrGreater) { + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope], " + + s"[create] privilege on [$db1/$table2/id,$db1/$table2/scope], " + + s"[write] privilege on [[$path, $path/]]" + } else { + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/scope], " + + s"[create] privilege on [$db1/$table2/id,$db1/$table2/scope], " + + s"[write] privilege on [[file://$path, file://$path/]]" + }) + } + } + } + } + + test("[KYUUBI #5677][AUTHZ] Typeof expression miss column information") { + val db1 = defaultDb + val table1 = "table1" + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs( + admin, + sql( + s""" + |CREATE TABLE IF NOT EXISTS $db1.$table1( + |id int, + |scope int, + |day string) + |""".stripMargin)) + doAs(admin, sql(s"INSERT INTO $db1.$table1 SELECT 1, 2, 'TONY'")) + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql(s"SELECT typeof(id), typeof(typeof(day)) FROM $db1.$table1").collect()))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/day]") + interceptEndsWith[AccessControlException]( + doAs( + someone, + sql( + s""" + |SELECT + |typeof(cast(id as string)), + |typeof(substring(day, 1, 3)) + |FROM $db1.$table1""".stripMargin).collect()))( + s"does not have [select] privilege on [$db1/$table1/id,$db1/$table1/day]") + checkAnswer( + admin, + s""" + |SELECT + |typeof(id), + |typeof(typeof(day)), + |typeof(cast(id as string)), + |typeof(substring(day, 1, 3)) + |FROM $db1.$table1""".stripMargin, + Seq(Row("int", "string", "string", "string"))) + } + } + } + + test("[KYUUBI #5692][Bug] Authz not skip explain command") { + val db1 = defaultDb + val table1 = "table1" + withSingleCallEnabled { + withCleanTmpResources(Seq((s"$db1.$table1", "table"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1 (id int, scope int)")) + val explainSql = + s""" + |EXPLAIN + |SELECT id FROM $db1.$table1 + |""".stripMargin + doAs(admin, sql(explainSql)) + val result = doAs(someone, sql(explainSql).collect()).head.getString(0) + assert(!result.contains("Error occurred during query planning")) + assert(!result.contains(s"does not have [select] privilege on [$db1/$table1/id]")) + interceptEndsWith[AccessControlException]( + doAs(someone, sql(s"SELECT id FROM $db1.$table1").collect()))( + s"does not have [select] privilege on [$db1/$table1/id]") + } + } + } + + test("[KYUUBI #5793][BUG] PVM with nested scala-subquery should not src table privilege") { + val db1 = defaultDb + val table1 = "table1" + val table2 = "table2" + val table3 = "table3" + val view1 = "perm_view" + withSingleCallEnabled { + withCleanTmpResources( + Seq( + (s"$db1.$table1", "table"), + (s"$db1.$table2", "table"), + (s"$db1.$table3", "table"), + (s"$db1.$view1", "view"))) { + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1(id int, scope int)")) + doAs( + admin, + sql( + s""" + | CREATE TABLE IF NOT EXISTS $db1.$table2( + | id int, + | name string, + | age int, + | scope int) + | """.stripMargin)) + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table3(id int, scope int)")) + doAs( + admin, + sql( + s""" + |CREATE VIEW $db1.$view1 + |AS + |SELECT id, name, max(scope) as max_scope, sum(age) sum_age + |FROM $db1.$table2 + |WHERE scope in ( + | SELECT max(scope) max_scope + | FROM $db1.$table1 + | WHERE id IN (SELECT id FROM $db1.$table3) + |) + |GROUP BY id, name + |""".stripMargin)) + + checkAnswer(permViewOnlyUser, s"SELECT * FROM $db1.$view1", Array.empty[Row]) + } + } + } + + test("[KYUUBI #5884] PVM should inherit MultiInstance and wrap with new exprId") { + val db1 = defaultDb + val table1 = "table1" + val perm_view = "perm_view" + val view1 = "view1" + val view2 = "view2" + val view3 = "view3" + withSingleCallEnabled { + withCleanTmpResources(Seq.empty) { + sql("set spark.sql.legacy.storeAnalyzedPlanForView=true") + doAs(admin, sql(s"CREATE TABLE IF NOT EXISTS $db1.$table1(id int, scope int)")) + doAs(admin, sql(s"CREATE VIEW $db1.$perm_view AS SELECT * FROM $db1.$table1")) + + doAs( + admin, + sql( + s""" + |CREATE OR REPLACE TEMPORARY VIEW $view1 AS + |SELECT * + |FROM $db1.$perm_view + |WHERE id > 10 + |""".stripMargin)) + + doAs( + admin, + sql( + s""" + |CREATE OR REPLACE TEMPORARY VIEW $view2 AS + |SELECT * + |FROM $view1 + |WHERE scope < 10 + |""".stripMargin)) + + doAs( + admin, + sql( + s""" + |CREATE OR REPLACE TEMPORARY VIEW $view3 AS + |SELECT * + |FROM $view1 + |WHERE scope is not null + |""".stripMargin)) + + interceptContains[AccessControlException]( + doAs( + someone, + sql( + s""" + |SELECT a.*, b.scope as new_scope + |FROM $view2 a + |JOIN $view3 b + |ON a.id == b.id + |""".stripMargin).collect()))(s"does not have [select] privilege on " + + s"[$db1/$perm_view/id,$db1/$perm_view/scope,$db1/$perm_view/scope,$db1/$perm_view/id]") } } } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationCheckerSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/AuthzConfigurationCheckerSuite.scala similarity index 92% rename from extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationCheckerSuite.scala rename to extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/AuthzConfigurationCheckerSuite.scala index cd5757e545b..10fa0af9e1c 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/ranger/AuthzConfigurationCheckerSuite.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/AuthzConfigurationCheckerSuite.scala @@ -15,13 +15,15 @@ * limitations under the License. */ -package org.apache.kyuubi.plugin.spark.authz.ranger +package org.apache.kyuubi.plugin.spark.authz.rule import org.scalatest.BeforeAndAfterAll // scalastyle:off import org.scalatest.funsuite.AnyFunSuite import org.apache.kyuubi.plugin.spark.authz.{AccessControlException, SparkSessionProvider} +import org.apache.kyuubi.plugin.spark.authz.ranger.RuleAuthorization +import org.apache.kyuubi.plugin.spark.authz.rule.config.AuthzConfigurationChecker class AuthzConfigurationCheckerSuite extends AnyFunSuite with SparkSessionProvider with BeforeAndAfterAll { diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/KyuubiTPCDSResults.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/KyuubiTPCDSResults.scala new file mode 100644 index 00000000000..b119190091e --- /dev/null +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/KyuubiTPCDSResults.scala @@ -0,0 +1,167 @@ +/* + * 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.kyuubi.spark.connector.tpcds + +import java.lang.{Iterable => JIterable} +import java.lang.reflect.InvocationTargetException +import java.util.{Iterator => JIterator} + +import com.google.common.collect.AbstractIterator +import io.trino.tpcds._ +import io.trino.tpcds.`type`.{Decimal => TPCDSDecimal} +import io.trino.tpcds.row.generator.RowGenerator +import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.expressions.GenericInternalRow +import org.apache.spark.sql.catalyst.util.{DateTimeUtils, RebaseDateTime} +import org.apache.spark.sql.types.{CharType, DateType, Decimal, DecimalType, IntegerType, LongType, StringType, StructType, VarcharType} +import org.apache.spark.unsafe.types.UTF8String + +import org.apache.kyuubi.spark.connector.tpcds.KyuubiResultsIterator.{FALSE_STRING, TRUE_STRING} +import org.apache.kyuubi.spark.connector.tpcds.row.KyuubiTableRows + +class KyuubiTPCDSResults( + val table: Table, + val startingRowNumber: Long, + val rowCount: Long, + val session: Session, + val schema: StructType) extends JIterable[InternalRow] { + + override def iterator: JIterator[InternalRow] = + new KyuubiResultsIterator(table, startingRowNumber, rowCount, session, schema) +} + +object KyuubiTPCDSResults { + def constructResults(table: Table, session: Session, schema: StructType): KyuubiTPCDSResults = { + val chunkBoundaries = io.trino.tpcds.Parallel.splitWork(table, session) + new KyuubiTPCDSResults( + table, + chunkBoundaries.getFirstRow(), + chunkBoundaries.getLastRow(), + session, + schema) + } +} + +class KyuubiResultsIterator( + val table: Table, + val startingRowNumber: Long, + val endingRowNumber: Long, + val session: Session, + val sparkSchema: StructType) extends AbstractIterator[InternalRow] { + private var rowNumber: Long = 0L + private var rowGenerator: RowGenerator = _ + private var parentRowGenerator: Option[RowGenerator] = None + private var childRowGenerator: Option[RowGenerator] = None + + try { + require(table != null, "table is null") + require(session != null, "session is null") + require(startingRowNumber >= 1, s"starting row number is less than 1: $startingRowNumber") + require( + endingRowNumber <= session.getScaling.getRowCount(table), + s"starting row number is greater than the total rows in $table: $endingRowNumber") + rowNumber = startingRowNumber + rowGenerator = table.getRowGeneratorClass().getDeclaredConstructor().newInstance() + parentRowGenerator = if (table.isChild()) { + Some(table.getParent().getRowGeneratorClass().getDeclaredConstructor().newInstance()) + } else None + childRowGenerator = if (table.hasChild()) { + Some(table.getChild().getRowGeneratorClass().getDeclaredConstructor().newInstance()) + } else None + } catch { + case e @ (_: NoSuchMethodException | + _: InstantiationException | + _: InvocationTargetException | + _: IllegalAccessException) => + throw new TpcdsException(e.toString()); + } + skipRowsUntilStartingRowNumber(startingRowNumber) + + private def skipRowsUntilStartingRowNumber(startingRowNumber: Long): Unit = { + rowGenerator.skipRowsUntilStartingRowNumber(startingRowNumber) + parentRowGenerator.foreach(_.skipRowsUntilStartingRowNumber(startingRowNumber)) + childRowGenerator.foreach(_.skipRowsUntilStartingRowNumber(startingRowNumber)) + } + + override protected def computeNext(): InternalRow = { + if (rowNumber > endingRowNumber) { + return endOfData + } + val result = rowGenerator.generateRowAndChildRows( + rowNumber, + session, + parentRowGenerator.orNull, + childRowGenerator.orNull) + var row: InternalRow = null + if (!result.getRowAndChildRows.isEmpty) { + row = toInternalRow(KyuubiTableRows.getValues(result.getRowAndChildRows.get(0))) + } + + if (result.shouldEndRow) { + rowStop() + rowNumber += 1 + } + if (result.getRowAndChildRows().isEmpty()) { + row = computeNext() + } + row + } + + private def rowStop(): Unit = { + rowGenerator.consumeRemainingSeedsForRow() + parentRowGenerator.foreach(_.consumeRemainingSeedsForRow()) + childRowGenerator.foreach(_.consumeRemainingSeedsForRow()) + } + + private val reusedRow = new Array[Any](sparkSchema.length) + + def toInternalRow(values: Array[Any]): InternalRow = { + var i = 0 + while (i < values.length) { + reusedRow(i) = (values(i), sparkSchema(i).dataType) match { + case (None | null, _) => null + case (Some(Options.DEFAULT_NULL_STRING), _) => null + case (Some(v: Boolean), _) => if (v) TRUE_STRING else FALSE_STRING + case (Some(v: Int), IntegerType) => v + case (Some(v: Long), IntegerType) => v.toInt + case (Some(v: Int), LongType) => v.toLong + case (Some(v: Long), LongType) => v + case (Some(v: Long), DateType) => + RebaseDateTime.rebaseJulianToGregorianDays(v.toInt) - DateTimeUtils.JULIAN_DAY_OF_EPOCH + case (Some(v), StringType) => UTF8String.fromString(v.toString) + case (Some(v), CharType(_)) => UTF8String.fromString(v.toString) + case (Some(v), VarcharType(_)) => UTF8String.fromString(v.toString) + case (Some(v: TPCDSDecimal), t: DecimalType) => + Decimal(v.getNumber, t.precision, t.scale) + case (Some(v: Int), t: DecimalType) => + val decimal = Decimal(v) + decimal.changePrecision(t.precision, t.scale) + decimal + case (Some(v), dt) => throw new IllegalArgumentException( + s"value: $v, value class: ${v.getClass.getName} type: $dt") + } + i += 1 + } + new GenericInternalRow(reusedRow) + } +} + +object KyuubiResultsIterator { + private val TRUE_STRING = UTF8String.fromString("Y") + private val FALSE_STRING = UTF8String.fromString("N") +} diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSBatchScan.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSBatchScan.scala index 291031c53c9..919e43342ac 100644 --- a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSBatchScan.scala +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSBatchScan.scala @@ -17,20 +17,15 @@ package org.apache.kyuubi.spark.connector.tpcds -import java.time.LocalDate -import java.time.format.DateTimeFormatter import java.util.OptionalLong -import scala.collection.JavaConverters._ - import io.trino.tpcds._ import org.apache.spark.sql.SparkSession import org.apache.spark.sql.catalyst.InternalRow import org.apache.spark.sql.connector.read._ import org.apache.spark.sql.types._ -import org.apache.spark.unsafe.types.UTF8String -case class TPCDSTableChuck(table: String, scale: Double, parallelism: Int, index: Int) +case class TPCDSTableChunk(table: String, scale: Double, parallelism: Int, index: Int) extends InputPartition class TPCDSBatchScan( @@ -62,10 +57,10 @@ class TPCDSBatchScan( override def readSchema: StructType = schema override def planInputPartitions: Array[InputPartition] = - (1 to parallelism).map { i => TPCDSTableChuck(table.getName, scale, parallelism, i) }.toArray + (1 to parallelism).map { i => TPCDSTableChunk(table.getName, scale, parallelism, i) }.toArray def createReaderFactory: PartitionReaderFactory = (partition: InputPartition) => { - val chuck = partition.asInstanceOf[TPCDSTableChuck] + val chuck = partition.asInstanceOf[TPCDSTableChunk] new TPCDSPartitionReader(chuck.table, chuck.scale, chuck.parallelism, chuck.index, schema) } @@ -90,32 +85,9 @@ class TPCDSPartitionReader( opt.toSession.withChunkNumber(index) } - private lazy val dateFmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - - private val reusedRow = new Array[Any](schema.length) - private val iterator = Results - .constructResults(chuckInfo.getOnlyTableToGenerate, chuckInfo) - .iterator.asScala - .map { _.get(0).asScala } // the 1st row is specific table row - .map { stringRow => - var i = 0 - while (i < stringRow.length) { - reusedRow(i) = (stringRow(i), schema(i).dataType) match { - case (null, _) => null - case (Options.DEFAULT_NULL_STRING, _) => null - case (v, IntegerType) => v.toInt - case (v, LongType) => v.toLong - case (v, DateType) => LocalDate.parse(v, dateFmt).toEpochDay.toInt - case (v, StringType) => UTF8String.fromString(v) - case (v, CharType(_)) => UTF8String.fromString(v) - case (v, VarcharType(_)) => UTF8String.fromString(v) - case (v, DecimalType()) => Decimal(v) - case (v, dt) => throw new IllegalArgumentException(s"value: $v, type: $dt") - } - i += 1 - } - InternalRow(reusedRow: _*) - } + private val iterator = KyuubiTPCDSResults + .constructResults(chuckInfo.getOnlyTableToGenerate, chuckInfo, schema) + .iterator private var currentRow: InternalRow = _ diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSConf.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSConf.scala index dbd22dc1a97..3edbfaebf22 100644 --- a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSConf.scala +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSConf.scala @@ -81,5 +81,5 @@ object TPCDSConf { val TPCDS_CONNECTOR_READ_CONF_PREFIX = s"$TPCDS_CONNECTOR_CONF_PREFIX.read" val MAX_PARTITION_BYTES_CONF = "maxPartitionBytes" - val MAX_PARTITION_BYTES_DEFAULT = "128m" + val MAX_PARTITION_BYTES_DEFAULT = "384m" } diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/row/KyuubiTableRows.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/row/KyuubiTableRows.scala new file mode 100644 index 00000000000..544498d6e1e --- /dev/null +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/main/scala/org/apache/kyuubi/spark/connector/tpcds/row/KyuubiTableRows.scala @@ -0,0 +1,1549 @@ +/* + * 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.kyuubi.spark.connector.tpcds.row + +import io.trino.tpcds.`type`.{Address, Decimal => TPCDSDecimal, Pricing} +import io.trino.tpcds.generator.CallCenterGeneratorColumn._ +import io.trino.tpcds.generator.CatalogPageGeneratorColumn._ +import io.trino.tpcds.generator.CatalogReturnsGeneratorColumn._ +import io.trino.tpcds.generator.CatalogSalesGeneratorColumn._ +import io.trino.tpcds.generator.CustomerAddressGeneratorColumn._ +import io.trino.tpcds.generator.CustomerDemographicsGeneratorColumn._ +import io.trino.tpcds.generator.CustomerGeneratorColumn._ +import io.trino.tpcds.generator.DateDimGeneratorColumn._ +import io.trino.tpcds.generator.DbgenVersionGeneratorColumn._ +import io.trino.tpcds.generator.GeneratorColumn +import io.trino.tpcds.generator.HouseholdDemographicsGeneratorColumn._ +import io.trino.tpcds.generator.IncomeBandGeneratorColumn._ +import io.trino.tpcds.generator.InventoryGeneratorColumn._ +import io.trino.tpcds.generator.ItemGeneratorColumn._ +import io.trino.tpcds.generator.PromotionGeneratorColumn._ +import io.trino.tpcds.generator.ReasonGeneratorColumn._ +import io.trino.tpcds.generator.ShipModeGeneratorColumn._ +import io.trino.tpcds.generator.StoreGeneratorColumn._ +import io.trino.tpcds.generator.StoreReturnsGeneratorColumn._ +import io.trino.tpcds.generator.StoreSalesGeneratorColumn._ +import io.trino.tpcds.generator.TimeDimGeneratorColumn._ +import io.trino.tpcds.generator.WarehouseGeneratorColumn._ +import io.trino.tpcds.generator.WebPageGeneratorColumn._ +import io.trino.tpcds.generator.WebReturnsGeneratorColumn._ +import io.trino.tpcds.generator.WebSalesGeneratorColumn._ +import io.trino.tpcds.generator.WebSiteGeneratorColumn._ +import io.trino.tpcds.row.{CallCenterRow, CatalogPageRow, CatalogReturnsRow, CatalogSalesRow, CustomerAddressRow, CustomerDemographicsRow, CustomerRow, DateDimRow, DbgenVersionRow, HouseholdDemographicsRow, IncomeBandRow, InventoryRow, ItemRow, PromotionRow, ReasonRow, ShipModeRow, StoreReturnsRow, StoreRow, StoreSalesRow, TableRow, TableRowWithNulls, TimeDimRow, WarehouseRow, WebPageRow, WebReturnsRow, WebSalesRow, WebSiteRow} + +import org.apache.kyuubi.spark.connector.tpcds.row.KyuubiTPCDSTableRowWithNullsUtils._ +import org.apache.kyuubi.util.reflect.{DynFields, DynMethods} + +object KyuubiTableRows { + + implicit class StoreRowImplicits(storeRow: StoreRow) { + def getStoreSk: Long = StoreRowImplicits.storeSk.get(storeRow) + def getStoreId: String = StoreRowImplicits.storeId.get(storeRow) + def getRecStartDateId: Long = StoreRowImplicits.recStartDateId.get(storeRow) + def getRecEndDateId: Long = StoreRowImplicits.recEndDateId.get(storeRow) + def getClosedDateId: Long = StoreRowImplicits.closedDateId.get(storeRow) + def getStoreName: String = StoreRowImplicits.storeName.get(storeRow) + def getEmployees: Int = StoreRowImplicits.employees.get(storeRow) + def getFloorSpace: Int = StoreRowImplicits.floorSpace.get(storeRow) + def getHours: String = StoreRowImplicits.hours.get(storeRow) + def getStoreManager: String = StoreRowImplicits.storeManager.get(storeRow) + def getMarketId: Int = StoreRowImplicits.marketId.get(storeRow) + def getDTaxPercentage: TPCDSDecimal = StoreRowImplicits.dTaxPercentage.get(storeRow) + def getGeographyClass: String = StoreRowImplicits.geographyClass.get(storeRow) + def getMarketDesc: String = StoreRowImplicits.marketDesc.get(storeRow) + def getMarketManager: String = StoreRowImplicits.marketManager.get(storeRow) + def getDivisionId: Long = StoreRowImplicits.divisionId.get(storeRow) + def getDivisionName: String = StoreRowImplicits.divisionName.get(storeRow) + def getCompanyId: Long = StoreRowImplicits.companyId.get(storeRow) + def getCompanyName: String = StoreRowImplicits.companyName.get(storeRow) + def getAddress: Address = StoreRowImplicits.address.get(storeRow) + } + object StoreRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[StoreRow], field) + .buildChecked[T]() + + lazy val storeSk = invoke[Long]("storeSk") + lazy val storeId = invoke[String]("storeId") + lazy val recStartDateId = invoke[Long]("recStartDateId") + lazy val recEndDateId = invoke[Long]("recEndDateId") + lazy val closedDateId = invoke[Long]("closedDateId") + lazy val storeName = invoke[String]("storeName") + lazy val employees = invoke[Int]("employees") + lazy val floorSpace = invoke[Int]("floorSpace") + lazy val hours = invoke[String]("hours") + lazy val storeManager = invoke[String]("storeManager") + lazy val marketId = invoke[Int]("marketId") + lazy val dTaxPercentage = invoke[TPCDSDecimal]("dTaxPercentage") + lazy val geographyClass = invoke[String]("geographyClass") + lazy val marketDesc = invoke[String]("marketDesc") + lazy val marketManager = invoke[String]("marketManager") + lazy val divisionId = invoke[Long]("divisionId") + lazy val divisionName = invoke[String]("divisionName") + lazy val companyId = invoke[Long]("companyId") + lazy val companyName = invoke[String]("companyName") + lazy val address = invoke[Address]("address") + + def values(row: StoreRow): Array[Any] = Array( + getOrNullForKey(row, row.getStoreSk, W_STORE_SK), + getOrNull(row, row.getStoreId, W_STORE_ID), + getDateOrNullFromJulianDays(row, row.getRecStartDateId, W_STORE_REC_START_DATE_ID), + getDateOrNullFromJulianDays(row, row.getRecEndDateId, W_STORE_REC_END_DATE_ID), + getOrNullForKey(row, row.getClosedDateId, W_STORE_CLOSED_DATE_ID), + getOrNull(row, row.getStoreName, W_STORE_NAME), + getOrNull(row, row.getEmployees, W_STORE_EMPLOYEES), + getOrNull(row, row.getFloorSpace, W_STORE_FLOOR_SPACE), + getOrNull(row, row.getHours, W_STORE_HOURS), + getOrNull(row, row.getStoreManager, W_STORE_MANAGER), + getOrNull(row, row.getMarketId, W_STORE_MARKET_ID), + getOrNull(row, row.getGeographyClass, W_STORE_GEOGRAPHY_CLASS), + getOrNull(row, row.getMarketDesc, W_STORE_MARKET_DESC), + getOrNull(row, row.getMarketManager, W_STORE_MARKET_MANAGER), + getOrNullForKey(row, row.getDivisionId, W_STORE_DIVISION_ID), + getOrNull(row, row.getDivisionName, W_STORE_DIVISION_NAME), + getOrNullForKey(row, row.getCompanyId, W_STORE_COMPANY_ID), + getOrNull(row, row.getCompanyName, W_STORE_COMPANY_NAME), + getOrNull(row, row.getAddress.getStreetNumber, W_STORE_ADDRESS_STREET_NUM), + getOrNull(row, row.getAddress.getStreetName, W_STORE_ADDRESS_STREET_NAME1), + getOrNull(row, row.getAddress.getStreetType, W_STORE_ADDRESS_STREET_TYPE), + getOrNull(row, row.getAddress.getSuiteNumber, W_STORE_ADDRESS_SUITE_NUM), + getOrNull(row, row.getAddress.getCity, W_STORE_ADDRESS_CITY), + getOrNull(row, row.getAddress.getCounty, W_STORE_ADDRESS_COUNTY), + getOrNull(row, row.getAddress.getState, W_STORE_ADDRESS_STATE), + getOrNull( + row, + java.lang.String.format("%05d", row.getAddress.getZip.asInstanceOf[Object]), + W_STORE_ADDRESS_ZIP), + getOrNull(row, row.getAddress.getCountry, W_STORE_ADDRESS_COUNTRY), + getOrNull(row, row.getAddress.getGmtOffset, W_STORE_ADDRESS_GMT_OFFSET), + getOrNull(row, row.getDTaxPercentage, W_STORE_TAX_PERCENTAGE)) + } + + implicit class ReasonRowImplicits(reasonRow: ReasonRow) { + def getRReasonSk: Long = ReasonRowImplicits.rReasonSk.get(reasonRow) + def getRReasonId: String = ReasonRowImplicits.rReasonId.get(reasonRow) + def getRReasonDescription: String = ReasonRowImplicits.rReasonDescription.get(reasonRow) + } + + object ReasonRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[ReasonRow], field) + .buildChecked[T]() + lazy val rReasonSk = invoke[Long]("rReasonSk") + lazy val rReasonId = invoke[String]("rReasonId") + lazy val rReasonDescription = invoke[String]("rReasonDescription") + + def values(row: ReasonRow): Array[Any] = Array( + getOrNullForKey(row, row.getRReasonSk, R_REASON_SK), + getOrNull(row, row.getRReasonId, R_REASON_ID), + getOrNull(row, row.getRReasonDescription, R_REASON_DESCRIPTION)) + } + + implicit class DbgenVersionRowImplicits(dbgenVersionRow: DbgenVersionRow) { + def getDvVersion: String = DbgenVersionRowImplicits.dvVersion.get(dbgenVersionRow) + def getDvCreateDate: String = DbgenVersionRowImplicits.dvCreateDate.get(dbgenVersionRow) + def getDvCreateTime: String = DbgenVersionRowImplicits.dvCreateTime.get(dbgenVersionRow) + def getDvCmdlineArgs: String = DbgenVersionRowImplicits.dvCmdlineArgs.get(dbgenVersionRow) + } + + object DbgenVersionRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[DbgenVersionRow], field) + .buildChecked[T]() + + lazy val dvVersion = invoke[String]("dvVersion") + lazy val dvCreateDate = invoke[String]("dvCreateDate") + lazy val dvCreateTime = invoke[String]("dvCreateTime") + lazy val dvCmdlineArgs = invoke[String]("dvCmdlineArgs") + + def values(row: DbgenVersionRow): Array[Any] = Array( + getOrNull(row, row.getDvVersion, DV_VERSION), + getOrNull(row, row.getDvCreateDate, DV_CREATE_DATE), + getOrNull(row, row.getDvCreateTime, DV_CREATE_TIME), + getOrNull(row, row.getDvCmdlineArgs, DV_CMDLINE_ARGS)) + } + + implicit class ShipModeRowImplicits(shipModeRow: ShipModeRow) { + def getSmShipModeSk: Long = ShipModeRowImplicits.smShipModeSk.get(shipModeRow) + def getSmShipModeId: String = ShipModeRowImplicits.smShipModeId.get(shipModeRow) + def getSmType: String = ShipModeRowImplicits.smType.get(shipModeRow) + def getSmCode: String = ShipModeRowImplicits.smCode.get(shipModeRow) + def getSmCarrier: String = ShipModeRowImplicits.smCarrier.get(shipModeRow) + def getSmContract: String = ShipModeRowImplicits.smContract.get(shipModeRow) + } + + object ShipModeRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[ShipModeRow], field) + .buildChecked[T]() + + lazy val smShipModeSk = invoke[Long]("smShipModeSk") + lazy val smShipModeId = invoke[String]("smShipModeId") + lazy val smType = invoke[String]("smType") + lazy val smCode = invoke[String]("smCode") + lazy val smCarrier = invoke[String]("smCarrier") + lazy val smContract = invoke[String]("smContract") + + def values(row: ShipModeRow): Array[Any] = Array( + getOrNullForKey(row, row.getSmShipModeSk, SM_SHIP_MODE_SK), + getOrNull(row, row.getSmShipModeId, SM_SHIP_MODE_ID), + getOrNull(row, row.getSmType, SM_TYPE), + getOrNull(row, row.getSmCode, SM_CODE), + getOrNull(row, row.getSmCarrier, SM_CARRIER), + getOrNull(row, row.getSmContract, SM_CONTRACT)) + } + + implicit class IncomeBandRowImplicits(incomeBandRow: IncomeBandRow) { + def getIbIncomeBandId: Int = IncomeBandRowImplicits.ibIncomeBandId.get(incomeBandRow) + def getIbLowerBound: Int = IncomeBandRowImplicits.ibLowerBound.get(incomeBandRow) + def getIbUpperBound: Int = IncomeBandRowImplicits.ibUpperBound.get(incomeBandRow) + } + + object IncomeBandRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[IncomeBandRow], field) + .buildChecked[T]() + + lazy val ibIncomeBandId = invoke[Int]("ibIncomeBandId") + lazy val ibLowerBound = invoke[Int]("ibLowerBound") + lazy val ibUpperBound = invoke[Int]("ibUpperBound") + + def values(row: IncomeBandRow): Array[Any] = Array( + getOrNull(row, row.getIbIncomeBandId, IB_INCOME_BAND_ID), + getOrNull(row, row.getIbLowerBound, IB_LOWER_BOUND), + getOrNull(row, row.getIbUpperBound, IB_UPPER_BOUND)) + } + + implicit class ItemRowImplicits(itemRow: ItemRow) { + def getIItemSk: Long = ItemRowImplicits.iItemSk.get(itemRow) + def getIItemId: String = ItemRowImplicits.iItemId.get(itemRow) + def getIRecStartDateId: Long = ItemRowImplicits.iRecStartDateId.get(itemRow) + def getIRecEndDateId: Long = ItemRowImplicits.iRecEndDateId.get(itemRow) + def getIItemDesc: String = ItemRowImplicits.iItemDesc.get(itemRow) + def getICurrentPrice: TPCDSDecimal = ItemRowImplicits.iCurrentPrice.get(itemRow) + def getIWholesaleCost: TPCDSDecimal = ItemRowImplicits.iWholesaleCost.get(itemRow) + def getIBrandId: Long = ItemRowImplicits.iBrandId.get(itemRow) + def getIBrand: String = ItemRowImplicits.iBrand.get(itemRow) + def getIClassId: Long = ItemRowImplicits.iClassId.get(itemRow) + def getIClass: String = ItemRowImplicits.iClass.get(itemRow) + def getICategoryId: Long = ItemRowImplicits.iCategoryId.get(itemRow) + def getICategory: String = ItemRowImplicits.iCategory.get(itemRow) + def getIManufactId: Long = ItemRowImplicits.iManufactId.get(itemRow) + def getIManufact: String = ItemRowImplicits.iManufact.get(itemRow) + def getISize: String = ItemRowImplicits.iSize.get(itemRow) + def getIFormulation: String = ItemRowImplicits.iFormulation.get(itemRow) + def getIColor: String = ItemRowImplicits.iColor.get(itemRow) + def getIUnits: String = ItemRowImplicits.iUnits.get(itemRow) + def getIContainer: String = ItemRowImplicits.iContainer.get(itemRow) + def getIManagerId: Long = ItemRowImplicits.iManagerId.get(itemRow) + def getIProductName: String = ItemRowImplicits.iProductName.get(itemRow) + def getIPromoSk: Long = ItemRowImplicits.iPromoSk.get(itemRow) + } + + object ItemRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[ItemRow], field) + .buildChecked[T]() + + lazy val iItemSk = invoke[Long]("iItemSk") + lazy val iItemId = invoke[String]("iItemId") + lazy val iRecStartDateId = invoke[Long]("iRecStartDateId") + lazy val iRecEndDateId = invoke[Long]("iRecEndDateId") + lazy val iItemDesc = invoke[String]("iItemDesc") + lazy val iCurrentPrice = invoke[TPCDSDecimal]("iCurrentPrice") + lazy val iWholesaleCost = invoke[TPCDSDecimal]("iWholesaleCost") + lazy val iBrandId = invoke[Long]("iBrandId") + lazy val iBrand = invoke[String]("iBrand") + lazy val iClassId = invoke[Long]("iClassId") + lazy val iClass = invoke[String]("iClass") + lazy val iCategoryId = invoke[Long]("iCategoryId") + lazy val iCategory = invoke[String]("iCategory") + lazy val iManufactId = invoke[Long]("iManufactId") + lazy val iManufact = invoke[String]("iManufact") + lazy val iSize = invoke[String]("iSize") + lazy val iFormulation = invoke[String]("iFormulation") + lazy val iColor = invoke[String]("iColor") + lazy val iUnits = invoke[String]("iUnits") + lazy val iContainer = invoke[String]("iContainer") + lazy val iManagerId = invoke[Long]("iManagerId") + lazy val iProductName = invoke[String]("iProductName") + lazy val iPromoSk = invoke[Long]("iPromoSk") + + def values(row: ItemRow): Array[Any] = Array( + getOrNullForKey(row, row.getIItemSk, I_ITEM_SK), + getOrNull(row, row.getIItemId, I_ITEM_ID), + getDateOrNullFromJulianDays(row, row.getIRecStartDateId, I_REC_START_DATE_ID), + getDateOrNullFromJulianDays(row, row.getIRecEndDateId, I_REC_END_DATE_ID), + getOrNull(row, row.getIItemDesc, I_ITEM_DESC), + getOrNull(row, row.getICurrentPrice, I_CURRENT_PRICE), + getOrNull(row, row.getIWholesaleCost, I_WHOLESALE_COST), + getOrNullForKey(row, row.getIBrandId, I_BRAND_ID), + getOrNull(row, row.getIBrand, I_BRAND), + getOrNullForKey(row, row.getIClassId, I_CLASS_ID), + getOrNull(row, row.getIClass, I_CLASS), + getOrNullForKey(row, row.getICategoryId, I_CATEGORY_ID), + getOrNull(row, row.getICategory, I_CATEGORY), + getOrNullForKey(row, row.getIManufactId, I_MANUFACT_ID), + getOrNull(row, row.getIManufact, I_MANUFACT), + getOrNull(row, row.getISize, I_SIZE), + getOrNull(row, row.getIFormulation, I_FORMULATION), + getOrNull(row, row.getIColor, I_COLOR), + getOrNull(row, row.getIUnits, I_UNITS), + getOrNull(row, row.getIContainer, I_CONTAINER), + getOrNullForKey(row, row.getIManagerId, I_MANAGER_ID), + getOrNull(row, row.getIProductName, I_PRODUCT_NAME)) + } + + implicit class CustomerDemographicsRowImplicits( + customerDemographicsRow: CustomerDemographicsRow) { + def getCdDemoSk: Long = CustomerDemographicsRowImplicits.cdDemoSk.get(customerDemographicsRow) + def getCdGender: String = CustomerDemographicsRowImplicits.cdGender.get(customerDemographicsRow) + def getCdMaritalStatus: String = + CustomerDemographicsRowImplicits.cdMaritalStatus.get(customerDemographicsRow) + def getCdEducationStatus: String = + CustomerDemographicsRowImplicits.cdEducationStatus.get(customerDemographicsRow) + def getCdPurchaseEstimate: Int = + CustomerDemographicsRowImplicits.cdPurchaseEstimate.get(customerDemographicsRow) + def getCdCreditRating: String = + CustomerDemographicsRowImplicits.cdCreditRating.get(customerDemographicsRow) + def getCdDepCount: Int = + CustomerDemographicsRowImplicits.cdDepCount.get(customerDemographicsRow) + def getCdDepEmployedCount: Int = + CustomerDemographicsRowImplicits.cdDepEmployedCount.get(customerDemographicsRow) + def getCdDepCollegeCount: Int = + CustomerDemographicsRowImplicits.cdDepCollegeCount.get(customerDemographicsRow) + } + + object CustomerDemographicsRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CustomerDemographicsRow], field) + .buildChecked[T]() + + lazy val cdDemoSk = invoke[Long]("cdDemoSk") + lazy val cdGender = invoke[String]("cdGender") + lazy val cdMaritalStatus = invoke[String]("cdMaritalStatus") + lazy val cdEducationStatus = invoke[String]("cdEducationStatus") + lazy val cdPurchaseEstimate = invoke[Int]("cdPurchaseEstimate") + lazy val cdCreditRating = invoke[String]("cdCreditRating") + lazy val cdDepCount = invoke[Int]("cdDepCount") + lazy val cdDepEmployedCount = invoke[Int]("cdDepEmployedCount") + lazy val cdDepCollegeCount = invoke[Int]("cdDepCollegeCount") + + def values(row: CustomerDemographicsRow): Array[Any] = Array( + getOrNullForKey(row, row.getCdDemoSk, CD_DEMO_SK), + getOrNull(row, row.getCdGender, CD_GENDER), + getOrNull(row, row.getCdMaritalStatus, CD_MARITAL_STATUS), + getOrNull(row, row.getCdEducationStatus, CD_EDUCATION_STATUS), + getOrNull(row, row.getCdPurchaseEstimate, CD_PURCHASE_ESTIMATE), + getOrNull(row, row.getCdCreditRating, CD_CREDIT_RATING), + getOrNull(row, row.getCdDepCount, CD_DEP_COUNT), + getOrNull(row, row.getCdDepEmployedCount, CD_DEP_EMPLOYED_COUNT), + getOrNull(row, row.getCdDepCollegeCount, CD_DEP_COLLEGE_COUNT)) + } + + implicit class TimeDimRowImplicits(timeDimRow: TimeDimRow) { + def getTTimeSk: Long = TimeDimRowImplicits.tTimeSk.get(timeDimRow) + def getTTimeId: String = TimeDimRowImplicits.tTimeId.get(timeDimRow) + def getTTime: Int = TimeDimRowImplicits.tTime.get(timeDimRow) + def getTHour: Int = TimeDimRowImplicits.tHour.get(timeDimRow) + def getTMinute: Int = TimeDimRowImplicits.tMinute.get(timeDimRow) + def getTSecond: Int = TimeDimRowImplicits.tSecond.get(timeDimRow) + def getTAmPm: String = TimeDimRowImplicits.tAmPm.get(timeDimRow) + def getTShift: String = TimeDimRowImplicits.tShift.get(timeDimRow) + def getTSubShift: String = TimeDimRowImplicits.tSubShift.get(timeDimRow) + def getTMealTime: String = TimeDimRowImplicits.tMealTime.get(timeDimRow) + } + + object TimeDimRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[TimeDimRow], field) + .buildChecked[T]() + + lazy val tTimeSk = invoke[Long]("tTimeSk") + lazy val tTimeId = invoke[String]("tTimeId") + lazy val tTime = invoke[Int]("tTime") + lazy val tHour = invoke[Int]("tHour") + lazy val tMinute = invoke[Int]("tMinute") + lazy val tSecond = invoke[Int]("tSecond") + lazy val tAmPm = invoke[String]("tAmPm") + lazy val tShift = invoke[String]("tShift") + lazy val tSubShift = invoke[String]("tSubShift") + lazy val tMealTime = invoke[String]("tMealTime") + + def values(row: TimeDimRow): Array[Any] = Array( + getOrNullForKey(row, row.getTTimeSk, T_TIME_SK), + getOrNull(row, row.getTTimeId, T_TIME_ID), + getOrNull(row, row.getTTime, T_TIME), + getOrNull(row, row.getTHour, T_HOUR), + getOrNull(row, row.getTMinute, T_MINUTE), + getOrNull(row, row.getTSecond, T_SECOND), + getOrNull(row, row.getTAmPm, T_AM_PM), + getOrNull(row, row.getTShift, T_SHIFT), + getOrNull(row, row.getTSubShift, T_SUB_SHIFT), + getOrNull(row, row.getTMealTime, T_MEAL_TIME)) + } + + implicit class WebSiteRowImplicits(webSiteRow: WebSiteRow) { + def getWebSiteSk: Long = WebSiteRowImplicits.webSiteSk.get(webSiteRow) + def getWebSiteId: String = WebSiteRowImplicits.webSiteId.get(webSiteRow) + def getWebRecStartDateId: Long = WebSiteRowImplicits.webRecStartDateId.get(webSiteRow) + def getWebRecEndDateId: Long = WebSiteRowImplicits.webRecEndDateId.get(webSiteRow) + def getWebName: String = WebSiteRowImplicits.webName.get(webSiteRow) + def getWebOpenDate: Long = WebSiteRowImplicits.webOpenDate.get(webSiteRow) + def getWebCloseDate: Long = WebSiteRowImplicits.webCloseDate.get(webSiteRow) + def getWebClass: String = WebSiteRowImplicits.webClass.get(webSiteRow) + def getWebManager: String = WebSiteRowImplicits.webManager.get(webSiteRow) + def getWebMarketId: Int = WebSiteRowImplicits.webMarketId.get(webSiteRow) + def getWebMarketClass: String = WebSiteRowImplicits.webMarketClass.get(webSiteRow) + def getWebMarketDesc: String = WebSiteRowImplicits.webMarketDesc.get(webSiteRow) + def getWebMarketManager: String = WebSiteRowImplicits.webMarketManager.get(webSiteRow) + def getWebCompanyId: Int = WebSiteRowImplicits.webCompanyId.get(webSiteRow) + def getWebCompanyName: String = WebSiteRowImplicits.webCompanyName.get(webSiteRow) + def getWebAddress: Address = WebSiteRowImplicits.webAddress.get(webSiteRow) + def getWebTaxPercentage: TPCDSDecimal = WebSiteRowImplicits.webTaxPercentage.get(webSiteRow) + } + + object WebSiteRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[WebSiteRow], field) + .buildChecked[T]() + + lazy val webSiteSk = invoke[Long]("webSiteSk") + lazy val webSiteId = invoke[String]("webSiteId") + lazy val webRecStartDateId = invoke[Long]("webRecStartDateId") + lazy val webRecEndDateId = invoke[Long]("webRecEndDateId") + lazy val webName = invoke[String]("webName") + lazy val webOpenDate = invoke[Long]("webOpenDate") + lazy val webCloseDate = invoke[Long]("webCloseDate") + lazy val webClass = invoke[String]("webClass") + lazy val webManager = invoke[String]("webManager") + lazy val webMarketId = invoke[Int]("webMarketId") + lazy val webMarketClass = invoke[String]("webMarketClass") + lazy val webMarketDesc = invoke[String]("webMarketDesc") + lazy val webMarketManager = invoke[String]("webMarketManager") + lazy val webCompanyId = invoke[Int]("webCompanyId") + lazy val webCompanyName = invoke[String]("webCompanyName") + lazy val webAddress = invoke[Address]("webAddress") + lazy val webTaxPercentage = invoke[TPCDSDecimal]("webTaxPercentage") + + def values(row: WebSiteRow): Array[Any] = Array( + getOrNullForKey(row, row.getWebSiteSk, WEB_SITE_SK), + getOrNull(row, row.getWebSiteId, WEB_SITE_ID), + getDateOrNullFromJulianDays(row, row.getWebRecStartDateId, WEB_REC_START_DATE_ID), + getDateOrNullFromJulianDays(row, row.getWebRecEndDateId, WEB_REC_END_DATE_ID), + getOrNull(row, row.getWebName, WEB_NAME), + getOrNullForKey(row, row.getWebOpenDate, WEB_OPEN_DATE), + getOrNullForKey(row, row.getWebCloseDate, WEB_CLOSE_DATE), + getOrNull(row, row.getWebClass, WEB_CLASS), + getOrNull(row, row.getWebManager, WEB_MANAGER), + getOrNull(row, row.getWebMarketId, WEB_MARKET_ID), + getOrNull(row, row.getWebMarketClass, WEB_MARKET_CLASS), + getOrNull(row, row.getWebMarketDesc, WEB_MARKET_DESC), + getOrNull(row, row.getWebMarketManager, WEB_MARKET_MANAGER), + getOrNull(row, row.getWebCompanyId, WEB_COMPANY_ID), + getOrNull(row, row.getWebCompanyName, WEB_COMPANY_NAME), + getOrNull(row, row.getWebAddress.getStreetNumber(), WEB_ADDRESS_STREET_NUM), + getOrNull(row, row.getWebAddress.getStreetName(), WEB_ADDRESS_STREET_NAME1), + getOrNull(row, row.getWebAddress.getStreetType(), WEB_ADDRESS_STREET_TYPE), + getOrNull(row, row.getWebAddress.getSuiteNumber(), WEB_ADDRESS_SUITE_NUM), + getOrNull(row, row.getWebAddress.getCity(), WEB_ADDRESS_CITY), + getOrNull(row, row.getWebAddress.getCounty(), WEB_ADDRESS_COUNTY), + getOrNull(row, row.getWebAddress.getState(), WEB_ADDRESS_STATE), + getOrNull( + row, + java.lang.String.format("%05d", row.getWebAddress.getZip().asInstanceOf[Object]), + WEB_ADDRESS_ZIP), + getOrNull(row, row.getWebAddress.getCountry(), WEB_ADDRESS_COUNTRY), + getOrNull(row, row.getWebAddress.getGmtOffset(), WEB_ADDRESS_GMT_OFFSET), + getOrNull(row, row.getWebTaxPercentage, WEB_TAX_PERCENTAGE)) + } + + implicit class HouseholdDemographicsRowImplicits( + householdDemographicsRow: HouseholdDemographicsRow) { + def getHdDemoSk: Long = HouseholdDemographicsRowImplicits.hdDemoSk.get(householdDemographicsRow) + def getHdIncomeBandId: Long = + HouseholdDemographicsRowImplicits.hdIncomeBandId.get(householdDemographicsRow) + def getHdBuyPotential: String = + HouseholdDemographicsRowImplicits.hdBuyPotential.get(householdDemographicsRow) + def getHdDepCount: Int = + HouseholdDemographicsRowImplicits.hdDepCount.get(householdDemographicsRow) + def getHdVehicleCount: Int = + HouseholdDemographicsRowImplicits.hdVehicleCount.get(householdDemographicsRow) + } + + object HouseholdDemographicsRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[HouseholdDemographicsRow], field) + .buildChecked[T]() + + lazy val hdDemoSk = invoke[Long]("hdDemoSk") + lazy val hdIncomeBandId = invoke[Long]("hdIncomeBandId") + lazy val hdBuyPotential = invoke[String]("hdBuyPotential") + lazy val hdDepCount = invoke[Int]("hdDepCount") + lazy val hdVehicleCount = invoke[Int]("hdVehicleCount") + + def values(row: HouseholdDemographicsRow): Array[Any] = Array( + getOrNullForKey(row, row.getHdDemoSk, HD_DEMO_SK), + getOrNullForKey(row, row.getHdIncomeBandId, HD_INCOME_BAND_ID), + getOrNull(row, row.getHdBuyPotential, HD_BUY_POTENTIAL), + getOrNull(row, row.getHdDepCount, HD_DEP_COUNT), + getOrNull(row, row.getHdVehicleCount, HD_VEHICLE_COUNT)) + } + + implicit class PromotionRowImplicits(promotionRow: PromotionRow) { + def getPPromoSk: Long = PromotionRowImplicits.pPromoSk.get(promotionRow) + def getPPromoId: String = PromotionRowImplicits.pPromoId.get(promotionRow) + def getPStartDateId: Long = PromotionRowImplicits.pStartDateId.get(promotionRow) + def getPEndDateId: Long = PromotionRowImplicits.pEndDateId.get(promotionRow) + def getPItemSk: Long = PromotionRowImplicits.pItemSk.get(promotionRow) + def getPCost: TPCDSDecimal = PromotionRowImplicits.pCost.get(promotionRow) + def getPResponseTarget: Int = PromotionRowImplicits.pResponseTarget.get(promotionRow) + def getPPromoName: String = PromotionRowImplicits.pPromoName.get(promotionRow) + def isPChannelDmail: Boolean = PromotionRowImplicits.pChannelDmail.get(promotionRow) + def isPChannelEmail: Boolean = PromotionRowImplicits.pChannelEmail.get(promotionRow) + def isPChannelCatalog: Boolean = PromotionRowImplicits.pChannelCatalog.get(promotionRow) + def isPChannelTv: Boolean = PromotionRowImplicits.pChannelTv.get(promotionRow) + def isPChannelRadio: Boolean = PromotionRowImplicits.pChannelRadio.get(promotionRow) + def isPChannelPress: Boolean = PromotionRowImplicits.pChannelPress.get(promotionRow) + def isPChannelEvent: Boolean = PromotionRowImplicits.pChannelEvent.get(promotionRow) + def isPChannelDemo: Boolean = PromotionRowImplicits.pChannelDemo.get(promotionRow) + def getPChannelDetails: String = PromotionRowImplicits.pChannelDetails.get(promotionRow) + def getPPurpose: String = PromotionRowImplicits.pPurpose.get(promotionRow) + def isPDiscountActive: Boolean = PromotionRowImplicits.pDiscountActive.get(promotionRow) + } + + object PromotionRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[PromotionRow], field) + .buildChecked[T]() + + lazy val pPromoSk = invoke[Long]("pPromoSk") + lazy val pPromoId = invoke[String]("pPromoId") + lazy val pStartDateId = invoke[Long]("pStartDateId") + lazy val pEndDateId = invoke[Long]("pEndDateId") + lazy val pItemSk = invoke[Long]("pItemSk") + lazy val pCost = invoke[TPCDSDecimal]("pCost") + lazy val pResponseTarget = invoke[Int]("pResponseTarget") + lazy val pPromoName = invoke[String]("pPromoName") + lazy val pChannelDmail = invoke[Boolean]("pChannelDmail") + lazy val pChannelEmail = invoke[Boolean]("pChannelEmail") + lazy val pChannelCatalog = invoke[Boolean]("pChannelCatalog") + lazy val pChannelTv = invoke[Boolean]("pChannelTv") + lazy val pChannelRadio = invoke[Boolean]("pChannelRadio") + lazy val pChannelPress = invoke[Boolean]("pChannelPress") + lazy val pChannelEvent = invoke[Boolean]("pChannelEvent") + lazy val pChannelDemo = invoke[Boolean]("pChannelDemo") + lazy val pChannelDetails = invoke[String]("pChannelDetails") + lazy val pPurpose = invoke[String]("pPurpose") + lazy val pDiscountActive = invoke[Boolean]("pDiscountActive") + + def values(row: PromotionRow): Array[Any] = Array( + getOrNullForKey(row, row.getPPromoSk, P_PROMO_SK), + getOrNull(row, row.getPPromoId, P_PROMO_ID), + getOrNullForKey(row, row.getPStartDateId, P_START_DATE_ID), + getOrNullForKey(row, row.getPEndDateId, P_END_DATE_ID), + getOrNullForKey(row, row.getPItemSk, P_ITEM_SK), + getOrNull(row, row.getPCost, P_COST), + getOrNull(row, row.getPResponseTarget, P_RESPONSE_TARGET), + getOrNull(row, row.getPPromoName, P_PROMO_NAME), + getOrNullForBoolean(row, row.isPChannelDmail, P_CHANNEL_DMAIL), + getOrNullForBoolean(row, row.isPChannelEmail, P_CHANNEL_EMAIL), + getOrNullForBoolean(row, row.isPChannelCatalog, P_CHANNEL_CATALOG), + getOrNullForBoolean(row, row.isPChannelTv, P_CHANNEL_TV), + getOrNullForBoolean(row, row.isPChannelRadio, P_CHANNEL_RADIO), + getOrNullForBoolean(row, row.isPChannelPress, P_CHANNEL_PRESS), + getOrNullForBoolean(row, row.isPChannelEvent, P_CHANNEL_EVENT), + getOrNullForBoolean(row, row.isPChannelDemo, P_CHANNEL_DEMO), + getOrNull(row, row.getPChannelDetails, P_CHANNEL_DETAILS), + getOrNull(row, row.getPPurpose, P_PURPOSE), + getOrNullForBoolean(row, row.isPDiscountActive, P_DISCOUNT_ACTIVE)) + } + + implicit class CatalogPageRowImplicits(catalogPageRow: CatalogPageRow) { + def getCpCatalogPageSk: Long = CatalogPageRowImplicits.cpCatalogPageSk.get(catalogPageRow) + def getCpCatalogPageId: String = CatalogPageRowImplicits.cpCatalogPageId.get(catalogPageRow) + def getCpStartDateId: Long = CatalogPageRowImplicits.cpStartDateId.get(catalogPageRow) + def getCpEndDateId: Long = CatalogPageRowImplicits.cpEndDateId.get(catalogPageRow) + def getCpDepartment: String = CatalogPageRowImplicits.cpDepartment.get(catalogPageRow) + def getCpCatalogNumber: Int = CatalogPageRowImplicits.cpCatalogNumber.get(catalogPageRow) + def getCpCatalogPageNumber: Int = + CatalogPageRowImplicits.cpCatalogPageNumber.get(catalogPageRow) + def getCpDescription: String = CatalogPageRowImplicits.cpDescription.get(catalogPageRow) + def getCpType: String = CatalogPageRowImplicits.cpType.get(catalogPageRow) + } + + object CatalogPageRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CatalogPageRow], field) + .buildChecked[T]() + + lazy val cpCatalogPageSk = invoke[Long]("cpCatalogPageSk") + lazy val cpCatalogPageId = invoke[String]("cpCatalogPageId") + lazy val cpStartDateId = invoke[Long]("cpStartDateId") + lazy val cpEndDateId = invoke[Long]("cpEndDateId") + lazy val cpDepartment = invoke[String]("cpDepartment") + lazy val cpCatalogNumber = invoke[Int]("cpCatalogNumber") + lazy val cpCatalogPageNumber = invoke[Int]("cpCatalogPageNumber") + lazy val cpDescription = invoke[String]("cpDescription") + lazy val cpType = invoke[String]("cpType") + + def values(row: CatalogPageRow): Array[Any] = Array( + getOrNullForKey(row, row.getCpCatalogPageSk, CP_CATALOG_PAGE_SK), + getOrNull(row, row.getCpCatalogPageId, CP_CATALOG_PAGE_ID), + getOrNullForKey(row, row.getCpStartDateId, CP_START_DATE_ID), + getOrNullForKey(row, row.getCpEndDateId, CP_END_DATE_ID), + getOrNull(row, row.getCpDepartment, CP_DEPARTMENT), + getOrNull(row, row.getCpCatalogNumber, CP_CATALOG_NUMBER), + getOrNull(row, row.getCpCatalogPageNumber, CP_CATALOG_PAGE_NUMBER), + getOrNull(row, row.getCpDescription, CP_DESCRIPTION), + getOrNull(row, row.getCpType, CP_TYPE)) + } + + implicit class WebSalesRowImplicits(webSalesRow: WebSalesRow) { + def getWsSoldDateSk: Long = WebSalesRowImplicits.wsSoldDateSk.get(webSalesRow) + def getWsSoldTimeSk: Long = WebSalesRowImplicits.wsSoldTimeSk.get(webSalesRow) + def getWsShipDateSk: Long = WebSalesRowImplicits.wsShipDateSk.get(webSalesRow) + def getWsItemSk: Long = WebSalesRowImplicits.wsItemSk.get(webSalesRow) + def getWsBillCustomerSk: Long = WebSalesRowImplicits.wsBillCustomerSk.get(webSalesRow) + def getWsBillCdemoSk: Long = WebSalesRowImplicits.wsBillCdemoSk.get(webSalesRow) + def getWsBillHdemoSk: Long = WebSalesRowImplicits.wsBillHdemoSk.get(webSalesRow) + def getWsBillAddrSk: Long = WebSalesRowImplicits.wsBillAddrSk.get(webSalesRow) + def getWsShipCustomerSk: Long = WebSalesRowImplicits.wsShipCustomerSk.get(webSalesRow) + def getWsShipCdemoSk: Long = WebSalesRowImplicits.wsShipCdemoSk.get(webSalesRow) + def getWsShipHdemoSk: Long = WebSalesRowImplicits.wsShipHdemoSk.get(webSalesRow) + def getWsShipAddrSk: Long = WebSalesRowImplicits.wsShipAddrSk.get(webSalesRow) + def getWsWebPageSk: Long = WebSalesRowImplicits.wsWebPageSk.get(webSalesRow) + def getWsWebSiteSk: Long = WebSalesRowImplicits.wsWebSiteSk.get(webSalesRow) + def getWsShipModeSk: Long = WebSalesRowImplicits.wsShipModeSk.get(webSalesRow) + def getWsWarehouseSk: Long = WebSalesRowImplicits.wsWarehouseSk.get(webSalesRow) + def getWsPromoSk: Long = WebSalesRowImplicits.wsPromoSk.get(webSalesRow) + def getWsOrderNumber: Long = WebSalesRowImplicits.wsOrderNumber.get(webSalesRow) + def getWsPricing: Pricing = WebSalesRowImplicits.wsPricing.get(webSalesRow) + } + + object WebSalesRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[WebSalesRow], field) + .buildChecked[T]() + + lazy val wsSoldDateSk = invoke[Long]("wsSoldDateSk") + lazy val wsSoldTimeSk = invoke[Long]("wsSoldTimeSk") + lazy val wsShipDateSk = invoke[Long]("wsShipDateSk") + lazy val wsItemSk = invoke[Long]("wsItemSk") + lazy val wsBillCustomerSk = invoke[Long]("wsBillCustomerSk") + lazy val wsBillCdemoSk = invoke[Long]("wsBillCdemoSk") + lazy val wsBillHdemoSk = invoke[Long]("wsBillHdemoSk") + lazy val wsBillAddrSk = invoke[Long]("wsBillAddrSk") + lazy val wsShipCustomerSk = invoke[Long]("wsShipCustomerSk") + lazy val wsShipCdemoSk = invoke[Long]("wsShipCdemoSk") + lazy val wsShipHdemoSk = invoke[Long]("wsShipHdemoSk") + lazy val wsShipAddrSk = invoke[Long]("wsShipAddrSk") + lazy val wsWebPageSk = invoke[Long]("wsWebPageSk") + lazy val wsWebSiteSk = invoke[Long]("wsWebSiteSk") + lazy val wsShipModeSk = invoke[Long]("wsShipModeSk") + lazy val wsWarehouseSk = invoke[Long]("wsWarehouseSk") + lazy val wsPromoSk = invoke[Long]("wsPromoSk") + lazy val wsOrderNumber = invoke[Long]("wsOrderNumber") + lazy val wsPricing = invoke[Pricing]("wsPricing") + + def values(row: WebSalesRow): Array[Any] = Array( + getOrNullForKey(row, row.getWsSoldDateSk, WS_SOLD_DATE_SK), + getOrNullForKey(row, row.getWsSoldTimeSk, WS_SOLD_TIME_SK), + getOrNullForKey(row, row.getWsShipDateSk, WS_SHIP_DATE_SK), + getOrNullForKey(row, row.getWsItemSk, WS_ITEM_SK), + getOrNullForKey(row, row.getWsBillCustomerSk, WS_BILL_CUSTOMER_SK), + getOrNullForKey(row, row.getWsBillCdemoSk, WS_BILL_CDEMO_SK), + getOrNullForKey(row, row.getWsBillHdemoSk, WS_BILL_HDEMO_SK), + getOrNullForKey(row, row.getWsBillAddrSk, WS_BILL_ADDR_SK), + getOrNullForKey(row, row.getWsShipCustomerSk, WS_SHIP_CUSTOMER_SK), + getOrNullForKey(row, row.getWsShipCdemoSk, WS_SHIP_CDEMO_SK), + getOrNullForKey(row, row.getWsShipHdemoSk, WS_SHIP_HDEMO_SK), + getOrNullForKey(row, row.getWsShipAddrSk, WS_SHIP_ADDR_SK), + getOrNullForKey(row, row.getWsWebPageSk, WS_WEB_PAGE_SK), + getOrNullForKey(row, row.getWsWebSiteSk, WS_WEB_SITE_SK), + getOrNullForKey(row, row.getWsShipModeSk, WS_SHIP_MODE_SK), + getOrNullForKey(row, row.getWsWarehouseSk, WS_WAREHOUSE_SK), + getOrNullForKey(row, row.getWsPromoSk, WS_PROMO_SK), + getOrNullForKey(row, row.getWsOrderNumber, WS_ORDER_NUMBER), + getOrNull(row, row.getWsPricing.getQuantity(), WS_PRICING_QUANTITY), + getOrNull(row, row.getWsPricing.getWholesaleCost(), WS_PRICING_WHOLESALE_COST), + getOrNull(row, row.getWsPricing.getListPrice(), WS_PRICING_LIST_PRICE), + getOrNull(row, row.getWsPricing.getSalesPrice(), WS_PRICING_SALES_PRICE), + getOrNull(row, row.getWsPricing.getExtDiscountAmount(), WS_PRICING_EXT_DISCOUNT_AMT), + getOrNull(row, row.getWsPricing.getExtSalesPrice(), WS_PRICING_EXT_SALES_PRICE), + getOrNull(row, row.getWsPricing.getExtWholesaleCost(), WS_PRICING_EXT_WHOLESALE_COST), + getOrNull(row, row.getWsPricing.getExtListPrice(), WS_PRICING_EXT_LIST_PRICE), + getOrNull(row, row.getWsPricing.getExtTax(), WS_PRICING_EXT_TAX), + getOrNull(row, row.getWsPricing.getCouponAmount(), WS_PRICING_COUPON_AMT), + getOrNull(row, row.getWsPricing.getExtShipCost(), WS_PRICING_EXT_SHIP_COST), + getOrNull(row, row.getWsPricing.getNetPaid(), WS_PRICING_NET_PAID), + getOrNull(row, row.getWsPricing.getNetPaidIncludingTax(), WS_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getWsPricing.getNetPaidIncludingShipping(), WS_PRICING_NET_PAID_INC_SHIP), + getOrNull( + row, + row.getWsPricing.getNetPaidIncludingShippingAndTax(), + WS_PRICING_NET_PAID_INC_SHIP_TAX), + getOrNull(row, row.getWsPricing.getNetProfit(), WS_PRICING_NET_PROFIT)) + } + + implicit class StoreSalesRowImplicits(storeSalesRow: StoreSalesRow) { + def getSsSoldDateSk: Long = StoreSalesRowImplicits.ssSoldDateSk.get(storeSalesRow) + def getSsSoldTimeSk: Long = StoreSalesRowImplicits.ssSoldTimeSk.get(storeSalesRow) + def getSsSoldItemSk: Long = StoreSalesRowImplicits.ssSoldItemSk.get(storeSalesRow) + def getSsSoldCustomerSk: Long = StoreSalesRowImplicits.ssSoldCustomerSk.get(storeSalesRow) + def getSsSoldCdemoSk: Long = StoreSalesRowImplicits.ssSoldCdemoSk.get(storeSalesRow) + def getSsSoldHdemoSk: Long = StoreSalesRowImplicits.ssSoldHdemoSk.get(storeSalesRow) + def getSsSoldAddrSk: Long = StoreSalesRowImplicits.ssSoldAddrSk.get(storeSalesRow) + def getSsSoldStoreSk: Long = StoreSalesRowImplicits.ssSoldStoreSk.get(storeSalesRow) + def getSsSoldPromoSk: Long = StoreSalesRowImplicits.ssSoldPromoSk.get(storeSalesRow) + def getSsTicketNumber: Long = StoreSalesRowImplicits.ssTicketNumber.get(storeSalesRow) + def getSsPricing: Pricing = StoreSalesRowImplicits.ssPricing.get(storeSalesRow) + } + + object StoreSalesRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[StoreSalesRow], field) + .buildChecked[T]() + + lazy val ssSoldDateSk = invoke[Long]("ssSoldDateSk") + lazy val ssSoldTimeSk = invoke[Long]("ssSoldTimeSk") + lazy val ssSoldItemSk = invoke[Long]("ssSoldItemSk") + lazy val ssSoldCustomerSk = invoke[Long]("ssSoldCustomerSk") + lazy val ssSoldCdemoSk = invoke[Long]("ssSoldCdemoSk") + lazy val ssSoldHdemoSk = invoke[Long]("ssSoldHdemoSk") + lazy val ssSoldAddrSk = invoke[Long]("ssSoldAddrSk") + lazy val ssSoldStoreSk = invoke[Long]("ssSoldStoreSk") + lazy val ssSoldPromoSk = invoke[Long]("ssSoldPromoSk") + lazy val ssTicketNumber = invoke[Long]("ssTicketNumber") + lazy val ssPricing = invoke[Pricing]("ssPricing") + + def values(row: StoreSalesRow): Array[Any] = Array( + getOrNullForKey(row, row.getSsSoldDateSk, SS_SOLD_DATE_SK), + getOrNullForKey(row, row.getSsSoldTimeSk, SS_SOLD_TIME_SK), + getOrNullForKey(row, row.getSsSoldItemSk, SS_SOLD_ITEM_SK), + getOrNullForKey(row, row.getSsSoldCustomerSk, SS_SOLD_CUSTOMER_SK), + getOrNullForKey(row, row.getSsSoldCdemoSk, SS_SOLD_CDEMO_SK), + getOrNullForKey(row, row.getSsSoldHdemoSk, SS_SOLD_HDEMO_SK), + getOrNullForKey(row, row.getSsSoldAddrSk, SS_SOLD_ADDR_SK), + getOrNullForKey(row, row.getSsSoldStoreSk, SS_SOLD_STORE_SK), + getOrNullForKey(row, row.getSsSoldPromoSk, SS_SOLD_PROMO_SK), + getOrNullForKey(row, row.getSsTicketNumber, SS_TICKET_NUMBER), + getOrNull(row, row.getSsPricing.getQuantity(), SS_PRICING_QUANTITY), + getOrNull(row, row.getSsPricing.getWholesaleCost(), SS_PRICING_WHOLESALE_COST), + getOrNull(row, row.getSsPricing.getListPrice(), SS_PRICING_LIST_PRICE), + getOrNull(row, row.getSsPricing.getSalesPrice(), SS_PRICING_SALES_PRICE), + getOrNull(row, row.getSsPricing.getCouponAmount(), SS_PRICING_COUPON_AMT), + getOrNull(row, row.getSsPricing.getExtSalesPrice(), SS_PRICING_EXT_SALES_PRICE), + getOrNull(row, row.getSsPricing.getExtWholesaleCost(), SS_PRICING_EXT_WHOLESALE_COST), + getOrNull(row, row.getSsPricing.getExtListPrice(), SS_PRICING_EXT_LIST_PRICE), + getOrNull(row, row.getSsPricing.getExtTax(), SS_PRICING_EXT_TAX), + getOrNull(row, row.getSsPricing.getCouponAmount(), SS_PRICING_COUPON_AMT), + getOrNull(row, row.getSsPricing.getNetPaid(), SS_PRICING_NET_PAID), + getOrNull(row, row.getSsPricing.getNetPaidIncludingTax(), SS_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getSsPricing.getNetProfit(), SS_PRICING_NET_PROFIT)) + } + + implicit class InventoryRowImplicits(inventoryRow: InventoryRow) { + def getInvDateSk: Long = InventoryRowImplicits.invDateSk.get(inventoryRow) + def getInvItemSk: Long = InventoryRowImplicits.invItemSk.get(inventoryRow) + def getInvWarehouseSk: Long = InventoryRowImplicits.invWarehouseSk.get(inventoryRow) + def getInvQuantityOnHand: Int = InventoryRowImplicits.invQuantityOnHand.get(inventoryRow) + } + + object InventoryRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[InventoryRow], field) + .buildChecked[T]() + + lazy val invDateSk = invoke[Long]("invDateSk") + lazy val invItemSk = invoke[Long]("invItemSk") + lazy val invWarehouseSk = invoke[Long]("invWarehouseSk") + lazy val invQuantityOnHand = invoke[Int]("invQuantityOnHand") + + def values(row: InventoryRow): Array[Any] = Array( + getOrNullForKey(row, row.getInvDateSk, INV_DATE_SK), + getOrNullForKey(row, row.getInvItemSk, INV_ITEM_SK), + getOrNullForKey(row, row.getInvWarehouseSk, INV_WAREHOUSE_SK), + getOrNull(row, row.getInvQuantityOnHand, INV_QUANTITY_ON_HAND)) + } + + implicit class WebReturnsRowImplicits(webReturnsRow: WebReturnsRow) { + def getWrReturnedDateSk: Long = WebReturnsRowImplicits.wrReturnedDateSk.get(webReturnsRow) + def getWrReturnedTimeSk: Long = WebReturnsRowImplicits.wrReturnedTimeSk.get(webReturnsRow) + def getWrItemSk: Long = WebReturnsRowImplicits.wrItemSk.get(webReturnsRow) + def getWrRefundedCustomerSk: Long = + WebReturnsRowImplicits.wrRefundedCustomerSk.get(webReturnsRow) + def getWrRefundedCdemoSk: Long = WebReturnsRowImplicits.wrRefundedCdemoSk.get(webReturnsRow) + def getWrRefundedHdemoSk: Long = WebReturnsRowImplicits.wrRefundedHdemoSk.get(webReturnsRow) + def getWrRefundedAddrSk: Long = WebReturnsRowImplicits.wrRefundedAddrSk.get(webReturnsRow) + def getWrReturningCustomerSk: Long = + WebReturnsRowImplicits.wrReturningCustomerSk.get(webReturnsRow) + def getWrReturningCdemoSk: Long = WebReturnsRowImplicits.wrReturningCdemoSk.get(webReturnsRow) + def getWrReturningHdemoSk: Long = WebReturnsRowImplicits.wrReturningHdemoSk.get(webReturnsRow) + def getWrReturningAddrSk: Long = WebReturnsRowImplicits.wrReturningAddrSk.get(webReturnsRow) + def getWrWebPageSk: Long = WebReturnsRowImplicits.wrWebPageSk.get(webReturnsRow) + def getWrReasonSk: Long = WebReturnsRowImplicits.wrReasonSk.get(webReturnsRow) + def getWrOrderNumber: Long = WebReturnsRowImplicits.wrOrderNumber.get(webReturnsRow) + def getWrPricing: Pricing = WebReturnsRowImplicits.wrPricing.get(webReturnsRow) + } + + object WebReturnsRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[WebReturnsRow], field) + .buildChecked[T]() + + lazy val wrReturnedDateSk = invoke[Long]("wrReturnedDateSk") + lazy val wrReturnedTimeSk = invoke[Long]("wrReturnedTimeSk") + lazy val wrItemSk = invoke[Long]("wrItemSk") + lazy val wrRefundedCustomerSk = invoke[Long]("wrRefundedCustomerSk") + lazy val wrRefundedCdemoSk = invoke[Long]("wrRefundedCdemoSk") + lazy val wrRefundedHdemoSk = invoke[Long]("wrRefundedHdemoSk") + lazy val wrRefundedAddrSk = invoke[Long]("wrRefundedAddrSk") + lazy val wrReturningCustomerSk = invoke[Long]("wrReturningCustomerSk") + lazy val wrReturningCdemoSk = invoke[Long]("wrReturningCdemoSk") + lazy val wrReturningHdemoSk = invoke[Long]("wrReturningHdemoSk") + lazy val wrReturningAddrSk = invoke[Long]("wrReturningAddrSk") + lazy val wrWebPageSk = invoke[Long]("wrWebPageSk") + lazy val wrReasonSk = invoke[Long]("wrReasonSk") + lazy val wrOrderNumber = invoke[Long]("wrOrderNumber") + lazy val wrPricing = invoke[Pricing]("wrPricing") + + def values(row: WebReturnsRow): Array[Any] = Array( + getOrNullForKey(row, row.getWrReturnedDateSk, WR_RETURNED_DATE_SK), + getOrNullForKey(row, row.getWrReturnedTimeSk, WR_RETURNED_TIME_SK), + getOrNullForKey(row, row.getWrItemSk, WR_ITEM_SK), + getOrNullForKey(row, row.getWrRefundedCustomerSk, WR_REFUNDED_CUSTOMER_SK), + getOrNullForKey(row, row.getWrRefundedCdemoSk, WR_REFUNDED_CDEMO_SK), + getOrNullForKey(row, row.getWrRefundedHdemoSk, WR_REFUNDED_HDEMO_SK), + getOrNullForKey(row, row.getWrRefundedAddrSk, WR_REFUNDED_ADDR_SK), + getOrNullForKey(row, row.getWrReturningCustomerSk, WR_RETURNING_CUSTOMER_SK), + getOrNullForKey(row, row.getWrReturningCdemoSk, WR_RETURNING_CDEMO_SK), + getOrNullForKey(row, row.getWrReturningHdemoSk, WR_RETURNING_HDEMO_SK), + getOrNullForKey(row, row.getWrReturningAddrSk, WR_RETURNING_ADDR_SK), + getOrNullForKey(row, row.getWrWebPageSk, WR_WEB_PAGE_SK), + getOrNullForKey(row, row.getWrReasonSk, WR_REASON_SK), + getOrNullForKey(row, row.getWrOrderNumber, WR_ORDER_NUMBER), + getOrNull(row, row.getWrPricing.getQuantity(), WR_PRICING_QUANTITY), + getOrNull(row, row.getWrPricing.getNetPaid(), WR_PRICING_NET_PAID), + getOrNull(row, row.getWrPricing.getExtTax(), WR_PRICING_EXT_TAX), + getOrNull(row, row.getWrPricing.getNetPaidIncludingTax(), WR_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getWrPricing.getFee(), WR_PRICING_FEE), + getOrNull(row, row.getWrPricing.getExtShipCost(), WR_PRICING_EXT_SHIP_COST), + getOrNull(row, row.getWrPricing.getRefundedCash(), WR_PRICING_REFUNDED_CASH), + getOrNull(row, row.getWrPricing.getReversedCharge(), WR_PRICING_REVERSED_CHARGE), + getOrNull(row, row.getWrPricing.getStoreCredit(), WR_PRICING_STORE_CREDIT), + getOrNull(row, row.getWrPricing.getNetLoss(), WR_PRICING_NET_LOSS)) + } + + implicit class WarehouseRowImplicits(warehouseRow: WarehouseRow) { + def getWWarehouseSk: Long = WarehouseRowImplicits.wWarehouseSk.get(warehouseRow) + def getWWarehouseId: String = WarehouseRowImplicits.wWarehouseId.get(warehouseRow) + def getWWarehouseName: String = WarehouseRowImplicits.wWarehouseName.get(warehouseRow) + def getWWarehouseSqFt: Int = WarehouseRowImplicits.wWarehouseSqFt.get(warehouseRow) + def getWAddress: Address = WarehouseRowImplicits.wAddress.get(warehouseRow) + } + + object WarehouseRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[WarehouseRow], field) + .buildChecked[T]() + + lazy val wWarehouseSk = invoke[Long]("wWarehouseSk") + lazy val wWarehouseId = invoke[String]("wWarehouseId") + lazy val wWarehouseName = invoke[String]("wWarehouseName") + lazy val wWarehouseSqFt = invoke[Int]("wWarehouseSqFt") + lazy val wAddress = invoke[Address]("wAddress") + + def values(row: WarehouseRow): Array[Any] = Array( + getOrNullForKey(row, row.getWWarehouseSk, W_WAREHOUSE_SK), + getOrNull(row, row.getWWarehouseId, W_WAREHOUSE_ID), + getOrNull(row, row.getWWarehouseName, W_WAREHOUSE_NAME), + getOrNull(row, row.getWWarehouseSqFt, W_WAREHOUSE_SQ_FT), + getOrNull(row, row.getWAddress.getStreetNumber(), W_ADDRESS_STREET_NUM), + getOrNull(row, row.getWAddress.getStreetName(), W_ADDRESS_STREET_NAME1), + getOrNull(row, row.getWAddress.getStreetType(), W_ADDRESS_STREET_TYPE), + getOrNull(row, row.getWAddress.getSuiteNumber(), W_ADDRESS_SUITE_NUM), + getOrNull(row, row.getWAddress.getCity(), W_ADDRESS_CITY), + getOrNull(row, row.getWAddress.getCounty(), W_ADDRESS_COUNTY), + getOrNull(row, row.getWAddress.getState(), W_ADDRESS_STATE), + getOrNull( + row, + java.lang.String.format("%05d", row.getWAddress.getZip.asInstanceOf[Object]), + W_ADDRESS_ZIP), + getOrNull(row, row.getWAddress.getCountry(), W_ADDRESS_COUNTRY), + getOrNull(row, row.getWAddress.getGmtOffset(), W_ADDRESS_GMT_OFFSET)) + } + + implicit class CustomerRowImplicits(customerRow: CustomerRow) { + def getCCustomerSk: Long = CustomerRowImplicits.cCustomerSk.get(customerRow) + def getCCustomerId: String = CustomerRowImplicits.cCustomerId.get(customerRow) + def getCCurrentCdemoSk: Long = CustomerRowImplicits.cCurrentCdemoSk.get(customerRow) + def getCCurrentHdemoSk: Long = CustomerRowImplicits.cCurrentHdemoSk.get(customerRow) + def getCCurrentAddrSk: Long = CustomerRowImplicits.cCurrentAddrSk.get(customerRow) + def getCFirstShiptoDateId: Int = CustomerRowImplicits.cFirstShiptoDateId.get(customerRow) + def getCFirstSalesDateId: Int = CustomerRowImplicits.cFirstSalesDateId.get(customerRow) + def getCSalutation: String = CustomerRowImplicits.cSalutation.get(customerRow) + def getCFirstName: String = CustomerRowImplicits.cFirstName.get(customerRow) + def getCLastName: String = CustomerRowImplicits.cLastName.get(customerRow) + def isCPreferredCustFlag: Boolean = CustomerRowImplicits.cPreferredCustFlag.get(customerRow) + def getCBirthDay: Int = CustomerRowImplicits.cBirthDay.get(customerRow) + def getCBirthMonth: Int = CustomerRowImplicits.cBirthMonth.get(customerRow) + def getCBirthYear: Int = CustomerRowImplicits.cBirthYear.get(customerRow) + def getCBirthCountry: String = CustomerRowImplicits.cBirthCountry.get(customerRow) + def getCLogin: String = CustomerRowImplicits.cLogin.get(customerRow) + def getCEmailAddress: String = CustomerRowImplicits.cEmailAddress.get(customerRow) + def getCLastReviewDate: Int = CustomerRowImplicits.cLastReviewDate.get(customerRow) + } + + object CustomerRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CustomerRow], field) + .buildChecked[T]() + + lazy val cCustomerSk = invoke[Long]("cCustomerSk") + lazy val cCustomerId = invoke[String]("cCustomerId") + lazy val cCurrentCdemoSk = invoke[Long]("cCurrentCdemoSk") + lazy val cCurrentHdemoSk = invoke[Long]("cCurrentHdemoSk") + lazy val cCurrentAddrSk = invoke[Long]("cCurrentAddrSk") + lazy val cFirstShiptoDateId = invoke[Int]("cFirstShiptoDateId") + lazy val cFirstSalesDateId = invoke[Int]("cFirstSalesDateId") + lazy val cSalutation = invoke[String]("cSalutation") + lazy val cFirstName = invoke[String]("cFirstName") + lazy val cLastName = invoke[String]("cLastName") + lazy val cPreferredCustFlag = invoke[Boolean]("cPreferredCustFlag") + lazy val cBirthDay = invoke[Int]("cBirthDay") + lazy val cBirthMonth = invoke[Int]("cBirthMonth") + lazy val cBirthYear = invoke[Int]("cBirthYear") + lazy val cBirthCountry = invoke[String]("cBirthCountry") + lazy val cLogin = invoke[String]("cLogin") + lazy val cEmailAddress = invoke[String]("cEmailAddress") + lazy val cLastReviewDate = invoke[Int]("cLastReviewDate") + + def values(row: CustomerRow): Array[Any] = Array( + getOrNullForKey(row, row.getCCustomerSk, C_CUSTOMER_SK), + getOrNull(row, row.getCCustomerId, C_CUSTOMER_ID), + getOrNullForKey(row, row.getCCurrentCdemoSk, C_CURRENT_CDEMO_SK), + getOrNullForKey(row, row.getCCurrentHdemoSk, C_CURRENT_HDEMO_SK), + getOrNullForKey(row, row.getCCurrentAddrSk, C_CURRENT_ADDR_SK), + getOrNull(row, row.getCFirstShiptoDateId, C_FIRST_SHIPTO_DATE_ID), + getOrNull(row, row.getCFirstSalesDateId, C_FIRST_SALES_DATE_ID), + getOrNull(row, row.getCSalutation, C_SALUTATION), + getOrNull(row, row.getCFirstName, C_FIRST_NAME), + getOrNull(row, row.getCLastName, C_LAST_NAME), + getOrNullForBoolean(row, row.isCPreferredCustFlag, C_PREFERRED_CUST_FLAG), + getOrNull(row, row.getCBirthDay, C_BIRTH_DAY), + getOrNull(row, row.getCBirthMonth, C_BIRTH_MONTH), + getOrNull(row, row.getCBirthYear, C_BIRTH_YEAR), + getOrNull(row, row.getCBirthCountry, C_BIRTH_COUNTRY), + row.getCLogin, + getOrNull(row, row.getCEmailAddress, C_EMAIL_ADDRESS), + getOrNull(row, row.getCLastReviewDate, C_LAST_REVIEW_DATE)) + } + + implicit class StoreReturnsRowImplicits(storeReturnsRow: StoreReturnsRow) { + def getSrReturnedDateSk: Long = StoreReturnsRowImplicits.srReturnedDateSk.get(storeReturnsRow) + def getSrReturnedTimeSk: Long = StoreReturnsRowImplicits.srReturnedTimeSk.get(storeReturnsRow) + def getSrItemSk: Long = StoreReturnsRowImplicits.srItemSk.get(storeReturnsRow) + def getSrCustomerSk: Long = StoreReturnsRowImplicits.srCustomerSk.get(storeReturnsRow) + def getSrCdemoSk: Long = StoreReturnsRowImplicits.srCdemoSk.get(storeReturnsRow) + def getSrHdemoSk: Long = StoreReturnsRowImplicits.srHdemoSk.get(storeReturnsRow) + def getSrAddrSk: Long = StoreReturnsRowImplicits.srAddrSk.get(storeReturnsRow) + def getSrStoreSk: Long = StoreReturnsRowImplicits.srStoreSk.get(storeReturnsRow) + def getSrReasonSk: Long = StoreReturnsRowImplicits.srReasonSk.get(storeReturnsRow) + def getSrTicketNumber: Long = StoreReturnsRowImplicits.srTicketNumber.get(storeReturnsRow) + def getSrPricing: Pricing = StoreReturnsRowImplicits.srPricing.get(storeReturnsRow) + } + + object StoreReturnsRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[StoreReturnsRow], field) + .buildChecked[T]() + + lazy val srReturnedDateSk = invoke[Long]("srReturnedDateSk") + lazy val srReturnedTimeSk = invoke[Long]("srReturnedTimeSk") + lazy val srItemSk = invoke[Long]("srItemSk") + lazy val srCustomerSk = invoke[Long]("srCustomerSk") + lazy val srCdemoSk = invoke[Long]("srCdemoSk") + lazy val srHdemoSk = invoke[Long]("srHdemoSk") + lazy val srAddrSk = invoke[Long]("srAddrSk") + lazy val srStoreSk = invoke[Long]("srStoreSk") + lazy val srReasonSk = invoke[Long]("srReasonSk") + lazy val srTicketNumber = invoke[Long]("srTicketNumber") + lazy val srPricing = invoke[Pricing]("srPricing") + + def values(row: StoreReturnsRow): Array[Any] = Array( + getOrNullForKey(row, row.getSrReturnedDateSk, SR_RETURNED_DATE_SK), + getOrNullForKey(row, row.getSrReturnedTimeSk, SR_RETURNED_TIME_SK), + getOrNullForKey(row, row.getSrItemSk, SR_ITEM_SK), + getOrNullForKey(row, row.getSrCustomerSk, SR_CUSTOMER_SK), + getOrNullForKey(row, row.getSrCdemoSk, SR_CDEMO_SK), + getOrNullForKey(row, row.getSrHdemoSk, SR_HDEMO_SK), + getOrNullForKey(row, row.getSrAddrSk, SR_ADDR_SK), + getOrNullForKey(row, row.getSrStoreSk, SR_STORE_SK), + getOrNullForKey(row, row.getSrReasonSk, SR_REASON_SK), + getOrNullForKey(row, row.getSrTicketNumber, SR_TICKET_NUMBER), + getOrNull(row, row.getSrPricing.getQuantity(), SR_PRICING_QUANTITY), + getOrNull(row, row.getSrPricing.getNetPaid(), SR_PRICING_NET_PAID), + getOrNull(row, row.getSrPricing.getExtTax(), SR_PRICING_EXT_TAX), + getOrNull(row, row.getSrPricing.getNetPaidIncludingTax(), SR_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getSrPricing.getFee(), SR_PRICING_FEE), + getOrNull(row, row.getSrPricing.getExtShipCost(), SR_PRICING_EXT_SHIP_COST), + getOrNull(row, row.getSrPricing.getRefundedCash(), SR_PRICING_REFUNDED_CASH), + getOrNull(row, row.getSrPricing.getReversedCharge(), SR_PRICING_REVERSED_CHARGE), + getOrNull(row, row.getSrPricing.getStoreCredit(), SR_PRICING_STORE_CREDIT), + getOrNull(row, row.getSrPricing.getNetLoss(), SR_PRICING_NET_LOSS)) + } + + implicit class CatalogReturnsRowImplicits(catalogReturnsRow: CatalogReturnsRow) { + def getCrReturnedDateSk: Long = + CatalogReturnsRowImplicits.crReturnedDateSk.get(catalogReturnsRow) + def getCrReturnedTimeSk: Long = + CatalogReturnsRowImplicits.crReturnedTimeSk.get(catalogReturnsRow) + def getCrItemSk: Long = CatalogReturnsRowImplicits.crItemSk.get(catalogReturnsRow) + def getCrRefundedCustomerSk: Long = + CatalogReturnsRowImplicits.crRefundedCustomerSk.get(catalogReturnsRow) + def getCrRefundedCdemoSk: Long = + CatalogReturnsRowImplicits.crRefundedCdemoSk.get(catalogReturnsRow) + def getCrRefundedHdemoSk: Long = + CatalogReturnsRowImplicits.crRefundedHdemoSk.get(catalogReturnsRow) + def getCrRefundedAddrSk: Long = + CatalogReturnsRowImplicits.crRefundedAddrSk.get(catalogReturnsRow) + def getCrReturningCustomerSk: Long = + CatalogReturnsRowImplicits.crReturningCustomerSk.get(catalogReturnsRow) + def getCrReturningCdemoSk: Long = + CatalogReturnsRowImplicits.crReturningCdemoSk.get(catalogReturnsRow) + def getCrReturningHdemoSk: Long = + CatalogReturnsRowImplicits.crReturningHdemoSk.get(catalogReturnsRow) + def getCrReturningAddrSk: Long = + CatalogReturnsRowImplicits.crReturningAddrSk.get(catalogReturnsRow) + def getCrCallCenterSk: Long = CatalogReturnsRowImplicits.crCallCenterSk.get(catalogReturnsRow) + def getCrCatalogPageSk: Long = CatalogReturnsRowImplicits.crCatalogPageSk.get(catalogReturnsRow) + def getCrShipModeSk: Long = CatalogReturnsRowImplicits.crShipModeSk.get(catalogReturnsRow) + def getCrWarehouseSk: Long = CatalogReturnsRowImplicits.crWarehouseSk.get(catalogReturnsRow) + def getCrReasonSk: Long = CatalogReturnsRowImplicits.crReasonSk.get(catalogReturnsRow) + def getCrOrderNumber: Long = CatalogReturnsRowImplicits.crOrderNumber.get(catalogReturnsRow) + def getCrPricing: Pricing = CatalogReturnsRowImplicits.crPricing.get(catalogReturnsRow) + } + + object CatalogReturnsRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CatalogReturnsRow], field) + .buildChecked[T]() + + lazy val crReturnedDateSk = invoke[Long]("crReturnedDateSk") + lazy val crReturnedTimeSk = invoke[Long]("crReturnedTimeSk") + lazy val crItemSk = invoke[Long]("crItemSk") + lazy val crRefundedCustomerSk = invoke[Long]("crRefundedCustomerSk") + lazy val crRefundedCdemoSk = invoke[Long]("crRefundedCdemoSk") + lazy val crRefundedHdemoSk = invoke[Long]("crRefundedHdemoSk") + lazy val crRefundedAddrSk = invoke[Long]("crRefundedAddrSk") + lazy val crReturningCustomerSk = invoke[Long]("crReturningCustomerSk") + lazy val crReturningCdemoSk = invoke[Long]("crReturningCdemoSk") + lazy val crReturningHdemoSk = invoke[Long]("crReturningHdemoSk") + lazy val crReturningAddrSk = invoke[Long]("crReturningAddrSk") + lazy val crCallCenterSk = invoke[Long]("crCallCenterSk") + lazy val crCatalogPageSk = invoke[Long]("crCatalogPageSk") + lazy val crShipModeSk = invoke[Long]("crShipModeSk") + lazy val crWarehouseSk = invoke[Long]("crWarehouseSk") + lazy val crReasonSk = invoke[Long]("crReasonSk") + lazy val crOrderNumber = invoke[Long]("crOrderNumber") + lazy val crPricing = invoke[Pricing]("crPricing") + + def values(row: CatalogReturnsRow): Array[Any] = Array( + getOrNullForKey(row, row.getCrReturnedDateSk, CR_RETURNED_DATE_SK), + getOrNullForKey(row, row.getCrReturnedTimeSk, CR_RETURNED_TIME_SK), + getOrNullForKey(row, row.getCrItemSk, CR_ITEM_SK), + getOrNullForKey(row, row.getCrRefundedCustomerSk, CR_REFUNDED_CUSTOMER_SK), + getOrNullForKey(row, row.getCrRefundedCdemoSk, CR_REFUNDED_CDEMO_SK), + getOrNullForKey(row, row.getCrRefundedHdemoSk, CR_REFUNDED_HDEMO_SK), + getOrNullForKey(row, row.getCrRefundedAddrSk, CR_REFUNDED_ADDR_SK), + getOrNullForKey(row, row.getCrReturningCustomerSk, CR_RETURNING_CUSTOMER_SK), + getOrNullForKey(row, row.getCrReturningCdemoSk, CR_RETURNING_CDEMO_SK), + getOrNullForKey(row, row.getCrReturningHdemoSk, CR_RETURNING_HDEMO_SK), + getOrNullForKey(row, row.getCrReturningAddrSk, CR_RETURNING_ADDR_SK), + getOrNullForKey(row, row.getCrCallCenterSk, CR_CALL_CENTER_SK), + getOrNullForKey(row, row.getCrCatalogPageSk, CR_CATALOG_PAGE_SK), + getOrNullForKey(row, row.getCrShipModeSk, CR_SHIP_MODE_SK), + getOrNullForKey(row, row.getCrWarehouseSk, CR_WAREHOUSE_SK), + getOrNullForKey(row, row.getCrReasonSk, CR_REASON_SK), + getOrNull(row, row.getCrOrderNumber, CR_ORDER_NUMBER), + getOrNull(row, row.getCrPricing.getQuantity(), CR_PRICING_QUANTITY), + getOrNull(row, row.getCrPricing.getNetPaid(), CR_PRICING_NET_PAID), + getOrNull(row, row.getCrPricing.getExtTax(), CR_PRICING_EXT_TAX), + getOrNull(row, row.getCrPricing.getNetPaidIncludingTax(), CR_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getCrPricing.getFee(), CR_PRICING_FEE), + getOrNull(row, row.getCrPricing.getExtShipCost(), CR_PRICING_EXT_SHIP_COST), + getOrNull(row, row.getCrPricing.getRefundedCash(), CR_PRICING_REFUNDED_CASH), + getOrNull(row, row.getCrPricing.getReversedCharge(), CR_PRICING_REVERSED_CHARGE), + getOrNull(row, row.getCrPricing.getStoreCredit(), CR_PRICING_STORE_CREDIT), + getOrNull(row, row.getCrPricing.getNetLoss(), CR_PRICING_NET_LOSS)) + } + + implicit class CatalogSalesRowImplicits(catalogSalesRow: CatalogSalesRow) { + def getCsSoldDateSk: Long = CatalogSalesRowImplicits.csSoldDateSk.get(catalogSalesRow) + def getCsSoldTimeSk: Long = CatalogSalesRowImplicits.csSoldTimeSk.get(catalogSalesRow) + def getCsShipDateSk: Long = CatalogSalesRowImplicits.csShipDateSk.get(catalogSalesRow) + def getCsBillCustomerSk: Long = CatalogSalesRowImplicits.csBillCustomerSk.get(catalogSalesRow) + def getCsBillCdemoSk: Long = CatalogSalesRowImplicits.csBillCdemoSk.get(catalogSalesRow) + def getCsBillHdemoSk: Long = CatalogSalesRowImplicits.csBillHdemoSk.get(catalogSalesRow) + def getCsBillAddrSk: Long = CatalogSalesRowImplicits.csBillAddrSk.get(catalogSalesRow) + def getCsShipCustomerSk: Long = CatalogSalesRowImplicits.csShipCustomerSk.get(catalogSalesRow) + def getCsShipCdemoSk: Long = CatalogSalesRowImplicits.csShipCdemoSk.get(catalogSalesRow) + def getCsShipHdemoSk: Long = CatalogSalesRowImplicits.csShipHdemoSk.get(catalogSalesRow) + def getCsShipAddrSk: Long = CatalogSalesRowImplicits.csShipAddrSk.get(catalogSalesRow) + def getCsCallCenterSk: Long = CatalogSalesRowImplicits.csCallCenterSk.get(catalogSalesRow) + def getCsCatalogPageSk: Long = CatalogSalesRowImplicits.csCatalogPageSk.get(catalogSalesRow) + def getCsShipModeSk: Long = CatalogSalesRowImplicits.csShipModeSk.get(catalogSalesRow) + def getCsWarehouseSk: Long = CatalogSalesRowImplicits.csWarehouseSk.get(catalogSalesRow) + def getCsSoldItemSk: Long = CatalogSalesRowImplicits.csSoldItemSk.get(catalogSalesRow) + def getCsPromoSk: Long = CatalogSalesRowImplicits.csPromoSk.get(catalogSalesRow) + def getCsOrderNumber: Long = CatalogSalesRowImplicits.csOrderNumber.get(catalogSalesRow) + def getCsPricing: Pricing = CatalogSalesRowImplicits.csPricing.get(catalogSalesRow) + } + + object CatalogSalesRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CatalogSalesRow], field) + .buildChecked[T]() + + lazy val csSoldDateSk = invoke[Long]("csSoldDateSk") + lazy val csSoldTimeSk = invoke[Long]("csSoldTimeSk") + lazy val csShipDateSk = invoke[Long]("csShipDateSk") + lazy val csBillCustomerSk = invoke[Long]("csBillCustomerSk") + lazy val csBillCdemoSk = invoke[Long]("csBillCdemoSk") + lazy val csBillHdemoSk = invoke[Long]("csBillHdemoSk") + lazy val csBillAddrSk = invoke[Long]("csBillAddrSk") + lazy val csShipCustomerSk = invoke[Long]("csShipCustomerSk") + lazy val csShipCdemoSk = invoke[Long]("csShipCdemoSk") + lazy val csShipHdemoSk = invoke[Long]("csShipHdemoSk") + lazy val csShipAddrSk = invoke[Long]("csShipAddrSk") + lazy val csCallCenterSk = invoke[Long]("csCallCenterSk") + lazy val csCatalogPageSk = invoke[Long]("csCatalogPageSk") + lazy val csShipModeSk = invoke[Long]("csShipModeSk") + lazy val csWarehouseSk = invoke[Long]("csWarehouseSk") + lazy val csSoldItemSk = invoke[Long]("csSoldItemSk") + lazy val csPromoSk = invoke[Long]("csPromoSk") + lazy val csOrderNumber = invoke[Long]("csOrderNumber") + lazy val csPricing = invoke[Pricing]("csPricing") + + def values(row: CatalogSalesRow): Array[Any] = Array( + getOrNullForKey(row, row.getCsSoldDateSk, CS_SOLD_DATE_SK), + getOrNullForKey(row, row.getCsSoldTimeSk, CS_SOLD_TIME_SK), + getOrNullForKey(row, row.getCsShipDateSk, CS_SHIP_DATE_SK), + getOrNullForKey(row, row.getCsBillCustomerSk, CS_BILL_CUSTOMER_SK), + getOrNullForKey(row, row.getCsBillCdemoSk, CS_BILL_CDEMO_SK), + getOrNullForKey(row, row.getCsBillHdemoSk, CS_BILL_HDEMO_SK), + getOrNullForKey(row, row.getCsBillAddrSk, CS_BILL_ADDR_SK), + getOrNullForKey(row, row.getCsShipCustomerSk, CS_SHIP_CUSTOMER_SK), + getOrNullForKey(row, row.getCsShipCdemoSk, CS_SHIP_CDEMO_SK), + getOrNullForKey(row, row.getCsShipHdemoSk, CS_SHIP_HDEMO_SK), + getOrNullForKey(row, row.getCsShipAddrSk, CS_SHIP_ADDR_SK), + getOrNullForKey(row, row.getCsCallCenterSk, CS_CALL_CENTER_SK), + getOrNullForKey(row, row.getCsCatalogPageSk, CS_CATALOG_PAGE_SK), + getOrNullForKey(row, row.getCsShipModeSk, CS_SHIP_MODE_SK), + getOrNull(row, row.getCsWarehouseSk, CS_WAREHOUSE_SK), + getOrNullForKey(row, row.getCsSoldItemSk, CS_SOLD_ITEM_SK), + getOrNullForKey(row, row.getCsPromoSk, CS_PROMO_SK), + getOrNull(row, row.getCsOrderNumber, CS_ORDER_NUMBER), + getOrNull(row, row.getCsPricing.getQuantity(), CS_PRICING_QUANTITY), + getOrNull(row, row.getCsPricing.getWholesaleCost(), CS_PRICING_WHOLESALE_COST), + getOrNull(row, row.getCsPricing.getListPrice(), CS_PRICING_LIST_PRICE), + getOrNull(row, row.getCsPricing.getSalesPrice(), CS_PRICING_SALES_PRICE), + getOrNull(row, row.getCsPricing.getExtDiscountAmount(), CS_PRICING_EXT_DISCOUNT_AMOUNT), + getOrNull(row, row.getCsPricing.getExtSalesPrice(), CS_PRICING_EXT_SALES_PRICE), + getOrNull(row, row.getCsPricing.getExtWholesaleCost(), CS_PRICING_EXT_WHOLESALE_COST), + getOrNull(row, row.getCsPricing.getExtListPrice(), CS_PRICING_EXT_LIST_PRICE), + getOrNull(row, row.getCsPricing.getExtTax(), CS_PRICING_EXT_TAX), + getOrNull(row, row.getCsPricing.getCouponAmount(), CS_PRICING_COUPON_AMT), + getOrNull(row, row.getCsPricing.getExtShipCost(), CS_PRICING_EXT_SHIP_COST), + getOrNull(row, row.getCsPricing.getNetPaid(), CS_PRICING_NET_PAID), + getOrNull(row, row.getCsPricing.getNetPaidIncludingTax(), CS_PRICING_NET_PAID_INC_TAX), + getOrNull(row, row.getCsPricing.getNetPaidIncludingShipping(), CS_PRICING_NET_PAID_INC_SHIP), + getOrNull( + row, + row.getCsPricing.getNetPaidIncludingShippingAndTax(), + CS_PRICING_NET_PAID_INC_SHIP_TAX), + getOrNull(row, row.getCsPricing.getNetProfit(), CS_PRICING_NET_PROFIT)) + } + + implicit class WebPageRowImplicits(webPageRow: WebPageRow) { + def getWpPageSk: Long = WebPageRowImplicits.wpPageSk.get(webPageRow) + def getWpPageId: String = WebPageRowImplicits.wpPageId.get(webPageRow) + def getWpRecStartDateId: Long = WebPageRowImplicits.wpRecStartDateId.get(webPageRow) + def getWpRecEndDateId: Long = WebPageRowImplicits.wpRecEndDateId.get(webPageRow) + def getWpCreationDateSk: Long = WebPageRowImplicits.wpCreationDateSk.get(webPageRow) + def getWpAccessDateSk: Long = WebPageRowImplicits.wpAccessDateSk.get(webPageRow) + def isWpAutogenFlag: Boolean = WebPageRowImplicits.wpAutogenFlag.get(webPageRow) + def getWpCustomerSk: Long = WebPageRowImplicits.wpCustomerSk.get(webPageRow) + def getWpUrl: String = WebPageRowImplicits.wpUrl.get(webPageRow) + def getWpType: String = WebPageRowImplicits.wpType.get(webPageRow) + def getWpCharCount: Int = WebPageRowImplicits.wpCharCount.get(webPageRow) + def getWpLinkCount: Int = WebPageRowImplicits.wpLinkCount.get(webPageRow) + def getWpImageCount: Int = WebPageRowImplicits.wpImageCount.get(webPageRow) + def getWpMaxAdCount: Int = WebPageRowImplicits.wpMaxAdCount.get(webPageRow) + } + + object WebPageRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[WebPageRow], field) + .buildChecked[T]() + + lazy val wpPageSk = invoke[Long]("wpPageSk") + lazy val wpPageId = invoke[String]("wpPageId") + lazy val wpRecStartDateId = invoke[Long]("wpRecStartDateId") + lazy val wpRecEndDateId = invoke[Long]("wpRecEndDateId") + lazy val wpCreationDateSk = invoke[Long]("wpCreationDateSk") + lazy val wpAccessDateSk = invoke[Long]("wpAccessDateSk") + lazy val wpAutogenFlag = invoke[Boolean]("wpAutogenFlag") + lazy val wpCustomerSk = invoke[Long]("wpCustomerSk") + lazy val wpUrl = invoke[String]("wpUrl") + lazy val wpType = invoke[String]("wpType") + lazy val wpCharCount = invoke[Int]("wpCharCount") + lazy val wpLinkCount = invoke[Int]("wpLinkCount") + lazy val wpImageCount = invoke[Int]("wpImageCount") + lazy val wpMaxAdCount = invoke[Int]("wpMaxAdCount") + + def values(row: WebPageRow): Array[Any] = Array( + getOrNullForKey(row, row.getWpPageSk, WP_PAGE_SK), + getOrNull(row, row.getWpPageId, WP_PAGE_ID), + getDateOrNullFromJulianDays(row, row.getWpRecStartDateId, WP_REC_START_DATE_ID), + getDateOrNullFromJulianDays(row, row.getWpRecEndDateId, WP_REC_END_DATE_ID), + getOrNullForKey(row, row.getWpCreationDateSk, WP_CREATION_DATE_SK), + getOrNullForKey(row, row.getWpAccessDateSk, WP_ACCESS_DATE_SK), + getOrNullForBoolean(row, row.isWpAutogenFlag, WP_AUTOGEN_FLAG), + getOrNullForKey(row, row.getWpCustomerSk, WP_CUSTOMER_SK), + getOrNull(row, row.getWpUrl, WP_URL), + getOrNull(row, row.getWpType, WP_TYPE), + getOrNull(row, row.getWpCharCount, WP_CHAR_COUNT), + getOrNull(row, row.getWpLinkCount, WP_LINK_COUNT), + getOrNull(row, row.getWpImageCount, WP_IMAGE_COUNT), + getOrNull(row, row.getWpMaxAdCount, WP_MAX_AD_COUNT)) + } + + implicit class CallCenterRowImplicits(callCenterRow: CallCenterRow) { + def getCcCallCenterSk: Long = CallCenterRowImplicits.ccCallCenterSk.get(callCenterRow) + def getCcCallCenterId: String = CallCenterRowImplicits.ccCallCenterId.get(callCenterRow) + def getCcRecStartDateId: Long = CallCenterRowImplicits.ccRecStartDateId.get(callCenterRow) + def getCcRecEndDateId: Long = CallCenterRowImplicits.ccRecEndDateId.get(callCenterRow) + def getCcClosedDateId: Long = CallCenterRowImplicits.ccClosedDateId.get(callCenterRow) + def getCcOpenDateId: Long = CallCenterRowImplicits.ccOpenDateId.get(callCenterRow) + def getCcName: String = CallCenterRowImplicits.ccName.get(callCenterRow) + def getCcClass: String = CallCenterRowImplicits.ccClass.get(callCenterRow) + def getCcEmployees: Int = CallCenterRowImplicits.ccEmployees.get(callCenterRow) + def getCcSqFt: Int = CallCenterRowImplicits.ccSqFt.get(callCenterRow) + def getCcHours: String = CallCenterRowImplicits.ccHours.get(callCenterRow) + def getCcManager: String = CallCenterRowImplicits.ccManager.get(callCenterRow) + def getCcMarketId: Int = CallCenterRowImplicits.ccMarketId.get(callCenterRow) + def getCcMarketClass: String = CallCenterRowImplicits.ccMarketClass.get(callCenterRow) + def getCcMarketDesc: String = CallCenterRowImplicits.ccMarketDesc.get(callCenterRow) + def getCcMarketManager: String = CallCenterRowImplicits.ccMarketManager.get(callCenterRow) + def getCcDivisionId: Int = CallCenterRowImplicits.ccDivisionId.get(callCenterRow) + def getCcDivisionName: String = CallCenterRowImplicits.ccDivisionName.get(callCenterRow) + def getCcCompany: Int = CallCenterRowImplicits.ccCompany.get(callCenterRow) + def getCcCompanyName: String = CallCenterRowImplicits.ccCompanyName.get(callCenterRow) + def getCcAddress: Address = CallCenterRowImplicits.ccAddress.get(callCenterRow) + def getCcTaxPercentage: TPCDSDecimal = CallCenterRowImplicits.ccTaxPercentage.get(callCenterRow) + } + + object CallCenterRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CallCenterRow], field) + .buildChecked[T]() + + lazy val ccCallCenterSk = invoke[Long]("ccCallCenterSk") + lazy val ccCallCenterId = invoke[String]("ccCallCenterId") + lazy val ccRecStartDateId = invoke[Long]("ccRecStartDateId") + lazy val ccRecEndDateId = invoke[Long]("ccRecEndDateId") + lazy val ccClosedDateId = invoke[Long]("ccClosedDateId") + lazy val ccOpenDateId = invoke[Long]("ccOpenDateId") + lazy val ccName = invoke[String]("ccName") + lazy val ccClass = invoke[String]("ccClass") + lazy val ccEmployees = invoke[Int]("ccEmployees") + lazy val ccSqFt = invoke[Int]("ccSqFt") + lazy val ccHours = invoke[String]("ccHours") + lazy val ccManager = invoke[String]("ccManager") + lazy val ccMarketId = invoke[Int]("ccMarketId") + lazy val ccMarketClass = invoke[String]("ccMarketClass") + lazy val ccMarketDesc = invoke[String]("ccMarketDesc") + lazy val ccMarketManager = invoke[String]("ccMarketManager") + lazy val ccDivisionId = invoke[Int]("ccDivisionId") + lazy val ccDivisionName = invoke[String]("ccDivisionName") + lazy val ccCompany = invoke[Int]("ccCompany") + lazy val ccCompanyName = invoke[String]("ccCompanyName") + lazy val ccAddress = invoke[Address]("ccAddress") + lazy val ccTaxPercentage = invoke[TPCDSDecimal]("ccTaxPercentage") + + def values(row: CallCenterRow): Array[Any] = Array( + getOrNullForKey(row, row.getCcCallCenterSk, CC_CALL_CENTER_SK), + getOrNull(row, row.getCcCallCenterId, CC_CALL_CENTER_ID), + getDateOrNullFromJulianDays(row, row.getCcRecStartDateId, CC_REC_START_DATE_ID), + getDateOrNullFromJulianDays(row, row.getCcRecEndDateId, CC_REC_END_DATE_ID), + getOrNullForKey(row, row.getCcClosedDateId, CC_CLOSED_DATE_ID), + getOrNullForKey(row, row.getCcOpenDateId, CC_OPEN_DATE_ID), + getOrNull(row, row.getCcName, CC_NAME), + getOrNull(row, row.getCcClass, CC_CLASS), + getOrNull(row, row.getCcEmployees, CC_EMPLOYEES), + getOrNull(row, row.getCcSqFt, CC_SQ_FT), + getOrNull(row, row.getCcHours, CC_HOURS), + getOrNull(row, row.getCcManager, CC_MANAGER), + getOrNull(row, row.getCcMarketId, CC_MARKET_ID), + getOrNull(row, row.getCcMarketClass, CC_MARKET_CLASS), + getOrNull(row, row.getCcMarketDesc, CC_MARKET_DESC), + getOrNull(row, row.getCcMarketManager, CC_MARKET_MANAGER), + getOrNull(row, row.getCcDivisionId, CC_DIVISION), + getOrNull(row, row.getCcDivisionName, CC_DIVISION_NAME), + getOrNull(row, row.getCcCompany, CC_COMPANY), + getOrNull(row, row.getCcCompanyName, CC_COMPANY_NAME), + getOrNull(row, row.getCcAddress.getStreetNumber, CC_STREET_NUMBER), + getOrNull(row, row.getCcAddress.getStreetName, CC_STREET_NAME), + getOrNull(row, row.getCcAddress.getStreetType, CC_STREET_TYPE), + getOrNull(row, row.getCcAddress.getSuiteNumber, CC_SUITE_NUMBER), + getOrNull(row, row.getCcAddress.getCity, CC_CITY), + getOrNull(row, row.getCcAddress.getCounty, CC_ADDRESS), + getOrNull(row, row.getCcAddress.getState, CC_STATE), + getOrNull( + row, + java.lang.String.format("%05d", row.getCcAddress.getZip.asInstanceOf[Object]), + CC_ZIP), + getOrNull(row, row.getCcAddress.getCountry, CC_COUNTRY), + getOrNull(row, row.getCcAddress.getGmtOffset, CC_GMT_OFFSET), + getOrNull(row, row.getCcTaxPercentage, CC_TAX_PERCENTAGE)) + } + + implicit class CustomerAddressRowImplicits(customerAddressRow: CustomerAddressRow) { + def getCaAddrSk: Long = CustomerAddressRowImplicits.caAddrSk.get(customerAddressRow) + def getCaAddrId: String = CustomerAddressRowImplicits.caAddrId.get(customerAddressRow) + def getCaAddress: Address = CustomerAddressRowImplicits.caAddress.get(customerAddressRow) + def getCaLocationType: String = + CustomerAddressRowImplicits.caLocationType.get(customerAddressRow) + } + + object CustomerAddressRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[CustomerAddressRow], field) + .buildChecked[T]() + + lazy val caAddrSk = invoke[Long]("caAddrSk") + lazy val caAddrId = invoke[String]("caAddrId") + lazy val caAddress = invoke[Address]("caAddress") + lazy val caLocationType = invoke[String]("caLocationType") + + def values(row: CustomerAddressRow): Array[Any] = Array( + getOrNullForKey(row, row.getCaAddrSk, CA_ADDRESS_SK), + getOrNull(row, row.getCaAddrId, CA_ADDRESS_ID), + getOrNull(row, row.getCaAddress.getStreetNumber(), CA_ADDRESS_STREET_NUM), + getOrNull(row, row.getCaAddress.getStreetName(), CA_ADDRESS_STREET_NAME), + getOrNull(row, row.getCaAddress.getStreetType(), CA_ADDRESS_STREET_TYPE), + getOrNull(row, row.getCaAddress.getSuiteNumber(), CA_ADDRESS_SUITE_NUM), + getOrNull(row, row.getCaAddress.getCity(), CA_ADDRESS_CITY), + getOrNull(row, row.getCaAddress.getCounty(), CA_ADDRESS_COUNTY), + getOrNull(row, row.getCaAddress.getState(), CA_ADDRESS_STATE), + getOrNull( + row, + java.lang.String.format("%05d", row.getCaAddress.getZip.asInstanceOf[Object]), + CA_ADDRESS_ZIP), + getOrNull(row, row.getCaAddress.getCountry(), CA_ADDRESS_COUNTRY), + getOrNull(row, row.getCaAddress.getGmtOffset(), CA_ADDRESS_GMT_OFFSET), + getOrNull(row, row.getCaLocationType, CA_LOCATION_TYPE)) + } + + implicit class DateDimRowImplicits(dateDimRow: DateDimRow) { + def getDDateSk: Long = DateDimRowImplicits.dDateSk.get(dateDimRow) + def getDDateId: String = DateDimRowImplicits.dDateId.get(dateDimRow) + def getDMonthSeq: Int = DateDimRowImplicits.dMonthSeq.get(dateDimRow) + def getDWeekSeq: Int = DateDimRowImplicits.dWeekSeq.get(dateDimRow) + def getDQuarterSeq: Int = DateDimRowImplicits.dQuarterSeq.get(dateDimRow) + def getDYear: Int = DateDimRowImplicits.dYear.get(dateDimRow) + def getDDow: Int = DateDimRowImplicits.dDow.get(dateDimRow) + def getDMoy: Int = DateDimRowImplicits.dMoy.get(dateDimRow) + def getDDom: Int = DateDimRowImplicits.dDom.get(dateDimRow) + def getDQoy: Int = DateDimRowImplicits.dQoy.get(dateDimRow) + def getDFyYear: Int = DateDimRowImplicits.dFyYear.get(dateDimRow) + def getDFyQuarterSeq: Int = DateDimRowImplicits.dFyQuarterSeq.get(dateDimRow) + def getDFyWeekSeq: Int = DateDimRowImplicits.dFyWeekSeq.get(dateDimRow) + def getDDayName: String = DateDimRowImplicits.dDayName.get(dateDimRow) + def isDHoliday: Boolean = DateDimRowImplicits.dHoliday.get(dateDimRow) + def isDWeekend: Boolean = DateDimRowImplicits.dWeekend.get(dateDimRow) + def isDFollowingHoliday: Boolean = DateDimRowImplicits.dFollowingHoliday.get(dateDimRow) + def getDFirstDom: Int = DateDimRowImplicits.dFirstDom.get(dateDimRow) + def getDLastDom: Int = DateDimRowImplicits.dLastDom.get(dateDimRow) + def getDSameDayLy: Int = DateDimRowImplicits.dSameDayLy.get(dateDimRow) + def getDSameDayLq: Int = DateDimRowImplicits.dSameDayLq.get(dateDimRow) + def isDCurrentDay: Boolean = DateDimRowImplicits.dCurrentDay.get(dateDimRow) + def isDCurrentWeek: Boolean = DateDimRowImplicits.dCurrentWeek.get(dateDimRow) + def isDCurrentMonth: Boolean = DateDimRowImplicits.dCurrentMonth.get(dateDimRow) + def isDCurrentQuarter: Boolean = DateDimRowImplicits.dCurrentQuarter.get(dateDimRow) + def isDCurrentYear: Boolean = DateDimRowImplicits.dCurrentYear.get(dateDimRow) + } + + object DateDimRowImplicits { + def invoke[T](field: String): DynFields.UnboundField[T] = + DynFields.builder() + .hiddenImpl(classOf[DateDimRow], field) + .buildChecked[T]() + + lazy val dDateSk = invoke[Long]("dDateSk") + lazy val dDateId = invoke[String]("dDateId") + lazy val dMonthSeq = invoke[Int]("dMonthSeq") + lazy val dWeekSeq = invoke[Int]("dWeekSeq") + lazy val dQuarterSeq = invoke[Int]("dQuarterSeq") + lazy val dYear = invoke[Int]("dYear") + lazy val dDow = invoke[Int]("dDow") + lazy val dMoy = invoke[Int]("dMoy") + lazy val dDom = invoke[Int]("dDom") + lazy val dQoy = invoke[Int]("dQoy") + lazy val dFyYear = invoke[Int]("dFyYear") + lazy val dFyQuarterSeq = invoke[Int]("dFyQuarterSeq") + lazy val dFyWeekSeq = invoke[Int]("dFyWeekSeq") + lazy val dDayName = invoke[String]("dDayName") + lazy val dHoliday = invoke[Boolean]("dHoliday") + lazy val dWeekend = invoke[Boolean]("dWeekend") + lazy val dFollowingHoliday = invoke[Boolean]("dFollowingHoliday") + lazy val dFirstDom = invoke[Int]("dFirstDom") + lazy val dLastDom = invoke[Int]("dLastDom") + lazy val dSameDayLy = invoke[Int]("dSameDayLy") + lazy val dSameDayLq = invoke[Int]("dSameDayLq") + lazy val dCurrentDay = invoke[Boolean]("dCurrentDay") + lazy val dCurrentWeek = invoke[Boolean]("dCurrentWeek") + lazy val dCurrentMonth = invoke[Boolean]("dCurrentMonth") + lazy val dCurrentQuarter = invoke[Boolean]("dCurrentQuarter") + lazy val dCurrentYear = invoke[Boolean]("dCurrentYear") + + def values(row: DateDimRow): Array[Any] = Array( + getOrNullForKey(row, row.getDDateSk, D_DATE_SK), + getOrNull(row, row.getDDateId, D_DATE_ID), + getDateOrNullFromJulianDays(row, row.getDDateSk, D_DATE_SK), + getOrNull(row, row.getDMonthSeq, D_MONTH_SEQ), + getOrNull(row, row.getDWeekSeq, D_WEEK_SEQ), + getOrNull(row, row.getDQuarterSeq, D_QUARTER_SEQ), + getOrNull(row, row.getDYear, D_YEAR), + getOrNull(row, row.getDDow, D_DOW), + getOrNull(row, row.getDMoy, D_MOY), + getOrNull(row, row.getDDom, D_DOM), + getOrNull(row, row.getDQoy, D_QOY), + getOrNull(row, row.getDFyYear, D_FY_YEAR), + getOrNull(row, row.getDFyQuarterSeq, D_FY_QUARTER_SEQ), + getOrNull(row, row.getDFyWeekSeq, D_FY_WEEK_SEQ), + getOrNull(row, row.getDDayName, D_DAY_NAME), + getOrNull( + row, + java.lang.String.format( + "%4dQ%d", + row.getDYear.asInstanceOf[Object], + row.getDQoy.asInstanceOf[Object]), + D_QUARTER_NAME), + getOrNullForBoolean(row, row.isDHoliday, D_HOLIDAY), + getOrNullForBoolean(row, row.isDWeekend, D_WEEKEND), + getOrNullForBoolean(row, row.isDFollowingHoliday, D_FOLLOWING_HOLIDAY), + getOrNull(row, row.getDFirstDom, D_FIRST_DOM), + getOrNull(row, row.getDLastDom, D_LAST_DOM), + getOrNull(row, row.getDSameDayLy, D_SAME_DAY_LY), + getOrNull(row, row.getDSameDayLq, D_SAME_DAY_LQ), + getOrNullForBoolean(row, row.isDCurrentDay, D_CURRENT_DAY), + getOrNullForBoolean(row, row.isDCurrentWeek, D_CURRENT_WEEK), + getOrNullForBoolean(row, row.isDCurrentMonth, D_CURRENT_MONTH), + getOrNullForBoolean(row, row.isDCurrentQuarter, D_CURRENT_QUARTER), + getOrNullForBoolean(row, row.isDCurrentYear, D_CURRENT_YEAR)) + } + + def getValues: TableRow => Array[Any] = { + case row: StoreRow => StoreRowImplicits.values(row) + case row: ReasonRow => ReasonRowImplicits.values(row) + case row: DbgenVersionRow => DbgenVersionRowImplicits.values(row) + case row: ShipModeRow => ShipModeRowImplicits.values(row) + case row: IncomeBandRow => IncomeBandRowImplicits.values(row) + case row: ItemRow => ItemRowImplicits.values(row) + case row: CustomerDemographicsRow => CustomerDemographicsRowImplicits.values(row) + case row: TimeDimRow => TimeDimRowImplicits.values(row) + case row: WebSiteRow => WebSiteRowImplicits.values(row) + case row: HouseholdDemographicsRow => HouseholdDemographicsRowImplicits.values(row) + case row: PromotionRow => PromotionRowImplicits.values(row) + case row: CatalogPageRow => CatalogPageRowImplicits.values(row) + case row: WebSalesRow => WebSalesRowImplicits.values(row) + case row: StoreSalesRow => StoreSalesRowImplicits.values(row) + case row: InventoryRow => InventoryRowImplicits.values(row) + case row: WebReturnsRow => WebReturnsRowImplicits.values(row) + case row: WarehouseRow => WarehouseRowImplicits.values(row) + case row: CustomerRow => CustomerRowImplicits.values(row) + case row: StoreReturnsRow => StoreReturnsRowImplicits.values(row) + case row: CatalogReturnsRow => CatalogReturnsRowImplicits.values(row) + case row: CatalogSalesRow => CatalogSalesRowImplicits.values(row) + case row: WebPageRow => WebPageRowImplicits.values(row) + case row: CallCenterRow => CallCenterRowImplicits.values(row) + case row: CustomerAddressRow => CustomerAddressRowImplicits.values(row) + case row: DateDimRow => DateDimRowImplicits.values(row) + } +} + +object KyuubiTPCDSTableRowWithNullsUtils { + private lazy val isNullMethod = DynMethods.builder("isNull") + .hiddenImpl( + classOf[TableRowWithNulls], + classOf[GeneratorColumn]) + .build() + + private def isNull( + row: TableRow, + column: GeneratorColumn): Boolean = isNullMethod.invoke[Boolean](row, column) + + def getDateOrNullFromJulianDays( + row: TableRow, + value: Long, + column: GeneratorColumn): Option[Long] = { + if (isNull(row, column) || value < 0) None else Some(value) + } + + def getOrNullForKey(row: TableRow, value: Long, column: GeneratorColumn): Option[Long] = { + if (isNull(row, column) || value == -1) None else Some(value) + } + + def getOrNull[T](row: TableRow, value: T, column: GeneratorColumn): Option[T] = { + if (isNull(row, column)) None else Some(value) + } + + def getOrNullForBoolean( + row: TableRow, + value: Boolean, + column: GeneratorColumn): Option[Boolean] = { + if (isNull(row, column)) None else Some(value) + } +} diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSCatalogSuite.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSCatalogSuite.scala index f5c6563e770..0eed970a4cd 100644 --- a/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSCatalogSuite.scala +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSCatalogSuite.scala @@ -19,6 +19,8 @@ package org.apache.kyuubi.spark.connector.tpcds import org.apache.spark.SparkConf import org.apache.spark.sql.{AnalysisException, SparkSession} +import org.apache.spark.sql.functions._ +import org.apache.spark.sql.types.DataTypes import org.apache.spark.sql.util.CaseInsensitiveStringMap import org.apache.kyuubi.KyuubiFunSuite @@ -77,42 +79,6 @@ class TPCDSCatalogSuite extends KyuubiFunSuite { } } - test("tpcds.tiny count") { - val sparkConf = new SparkConf() - .setMaster("local[*]") - .set("spark.ui.enabled", "false") - .set("spark.sql.catalogImplementation", "in-memory") - .set("spark.sql.catalog.tpcds", classOf[TPCDSCatalog].getName) - .set("spark.sql.cbo.enabled", "true") - .set("spark.sql.cbo.planStats.enabled", "true") - withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => - assert(spark.table("tpcds.tiny.call_center").count === 2) - assert(spark.table("tpcds.tiny.catalog_page").count === 11718) - assert(spark.table("tpcds.tiny.catalog_returns").count === 8923) - assert(spark.table("tpcds.tiny.catalog_sales").count === 89807) - assert(spark.table("tpcds.tiny.customer").count === 1000) - assert(spark.table("tpcds.tiny.customer_address").count === 1000) - assert(spark.table("tpcds.tiny.customer_demographics").count === 1920800) - assert(spark.table("tpcds.tiny.date_dim").count === 73049) - assert(spark.table("tpcds.tiny.household_demographics").count === 7200) - assert(spark.table("tpcds.tiny.income_band").count === 20) - assert(spark.table("tpcds.tiny.inventory").count === 261261) - assert(spark.table("tpcds.tiny.item").count === 2000) - assert(spark.table("tpcds.tiny.promotion").count === 3) - assert(spark.table("tpcds.tiny.reason").count === 1) - assert(spark.table("tpcds.tiny.ship_mode").count === 20) - assert(spark.table("tpcds.tiny.store").count === 2) - assert(spark.table("tpcds.tiny.store_returns").count === 11925) - assert(spark.table("tpcds.tiny.store_sales").count === 120527) - assert(spark.table("tpcds.tiny.time_dim").count === 86400) - assert(spark.table("tpcds.tiny.warehouse").count === 1) - assert(spark.table("tpcds.tiny.web_page").count === 2) - assert(spark.table("tpcds.tiny.web_returns").count === 1152) - assert(spark.table("tpcds.tiny.web_sales").count === 11876) - assert(spark.table("tpcds.tiny.web_site").count === 2) - } - } - test("tpcds.sf1 stats") { val sparkConf = new SparkConf() .setMaster("local[*]") @@ -174,4 +140,69 @@ class TPCDSCatalogSuite extends KyuubiFunSuite { || exception.message.contains("TABLE_OR_VIEW_NOT_FOUND")) } } + + test("tpcds.tiny count and checksum") { + val sparkConf = new SparkConf() + .setMaster("local[*]") + .set("spark.ui.enabled", "false") + .set("spark.sql.catalogImplementation", "in-memory") + .set("spark.sql.catalog.tpcds", classOf[TPCDSCatalog].getName) + .set("spark.sql.cbo.enabled", "true") + .set("spark.sql.cbo.planStats.enabled", "true") + withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => + tableInfo.foreach { + case (table, (expectCount, expectChecksum)) => + val (count, checksum) = countAndchecksum(spark, table) + assert(count == expectCount) + assert(checksum == expectChecksum, s"table $table") + } + } + } + + def countAndchecksum(spark: SparkSession, tableName: String): (String, String) = { + val df = spark.table(tableName) + val cols = df.schema.map { field => + concat( + when(col(field.name).isNull, lit('\u0000').cast("string")) + .otherwise(col(field.name).cast("string")), + lit('\u0001').cast("string")) + } + + df.select( + crc32(concat(cols: _*)) + .cast(DataTypes.createDecimalType(38, 0)) + .as("row_checksum")) + .agg( + count("*").cast("string").as("count"), + sum("row_checksum").cast("string").as("checksum")) + .collect() + .map(r => (r.getString(0), r.getString(1))) + .head + } + + private val tableInfo = Seq( + ("tpcds.tiny.call_center", ("2", "4584365911")), + ("tpcds.tiny.catalog_page", ("11718", "25416854987711")), + ("tpcds.tiny.catalog_returns", ("8923", "19045021547122")), + ("tpcds.tiny.catalog_sales", ("89807", "192355655243815")), + ("tpcds.tiny.customer", ("1000", "2120827330356")), + ("tpcds.tiny.customer_address", ("1000", "2161077976693")), + ("tpcds.tiny.customer_demographics", ("1920800", "4124183189708148")), + ("tpcds.tiny.date_dim", ("73049", "156926081012862")), + ("tpcds.tiny.household_demographics", ("7200", "15494873325812")), + ("tpcds.tiny.income_band", ("20", "41180951007")), + ("tpcds.tiny.inventory", ("261261", "561290989772724")), + ("tpcds.tiny.item", ("2000", "4254103006936")), + ("tpcds.tiny.promotion", ("3", "4984911899")), + ("tpcds.tiny.reason", ("1", "365440741")), + ("tpcds.tiny.ship_mode", ("20", "52349078860")), + ("tpcds.tiny.store", ("2", "2964682289")), + ("tpcds.tiny.store_returns", ("11925", "25400972943896")), + ("tpcds.tiny.store_sales", ("120527", "259296406856838")), + ("tpcds.tiny.time_dim", ("86400", "186045071019485")), + ("tpcds.tiny.warehouse", ("1", "2956768503")), + ("tpcds.tiny.web_page", ("2", "3215766118")), + ("tpcds.tiny.web_returns", ("1152", "2464383243098")), + ("tpcds.tiny.web_sales", ("11876", "25458905770096")), + ("tpcds.tiny.web_site", ("2", "3798438288"))) } diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.output.hash index 2d33a627d0b..74f1f32ede1 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ --2130215201 +-1796738616 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.sql index 5031eb86c10..b9b382350a2 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q1.sql @@ -19,13 +19,13 @@ select l_returnflag, l_linestatus, - sum(l_quantity) as sum_qty, - sum(l_extendedprice) as sum_base_price, - sum(l_extendedprice * (1 - l_discount)) as sum_disc_price, - sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge, - avg(l_quantity) as avg_qty, - avg(l_extendedprice) as avg_price, - avg(l_discount) as avg_disc, + round(sum(l_quantity), 2) as sum_qty, + round(sum(l_extendedprice), 2) as sum_base_price, + round(sum(l_extendedprice * (1 - l_discount)), 2) as sum_disc_price, + round(sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)), 2) as sum_charge, + round(avg(l_quantity), 2) as avg_qty, + round(avg(l_extendedprice), 2) as avg_price, + round(avg(l_discount), 2) as avg_disc, count(*) as count_order from lineitem diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.output.hash index 4a6454e519c..7b922f50772 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ --4090660469 +-730770831 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.sql index 87854a9ad60..89dd5247a1d 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q10.sql @@ -19,7 +19,7 @@ select c_custkey, c_name, - sum(l_extendedprice * (1 - l_discount)) as revenue, + round(sum(l_extendedprice * (1 - l_discount)), 1) as revenue, c_acctbal, n_name, c_address, diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.output.hash index 1b8cf626c59..e0595360402 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ -47333415 +799857942 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.sql index 4c5c485fe30..4aa7d13cab8 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q14.sql @@ -17,11 +17,11 @@ -- using default substitutions select - 100.00 * sum(case + round(100.00 * sum(case when p_type like 'PROMO%' then l_extendedprice * (1 - l_discount) else 0 - end) / sum(l_extendedprice * (1 - l_discount)) as promo_revenue + end) / sum(l_extendedprice * (1 - l_discount)), 2) as promo_revenue from lineitem, part diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.output.hash index 8dc6c1f42b0..a29b0607dfc 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ --2021679095 +-1401614325 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.sql index 52519586db1..11b9815fedc 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q15.sql @@ -34,7 +34,7 @@ select s_name, s_address, s_phone, - total_revenue + round(total_revenue, 2) as total_revenue from supplier, revenue0 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.output.hash index d931acdadfa..1d53348d252 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ -2111900859 +2123615405 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.sql index a9dea75f786..d504a839758 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q22.sql @@ -19,7 +19,7 @@ select cntrycode, count(*) as numcust, - sum(c_acctbal) as totacctbal + round(sum(c_acctbal), 2) as totacctbal from ( select diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.output.hash index d608f5fdd77..5320ce21257 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ -3717321142 +-2755325540 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.sql index fc998961c3b..847f372cce5 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q5.sql @@ -18,7 +18,7 @@ select n_name, - sum(l_extendedprice * (1 - l_discount)) as revenue + round(sum(l_extendedprice * (1 - l_discount)), 2) as revenue from customer, orders, diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.output.hash index c9efccd9e0c..fa86974aa11 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ -2062248569 +223845550 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.sql index efd8a50110e..3f9a7209446 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q6.sql @@ -17,7 +17,7 @@ -- using default substitutions select - sum(l_extendedprice * l_discount) as revenue + round(sum(l_extendedprice * l_discount), 2) as revenue from lineitem where diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.output.hash index 02a826f090c..29d09404524 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ --1955579146 +-95255706 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.sql index 3d932e660b5..7c565e6cdf6 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q7.sql @@ -20,7 +20,7 @@ select supp_nation, cust_nation, l_year, - sum(volume) as revenue + round(sum(volume), 2) as revenue from ( select diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.output.hash b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.output.hash index 9f13f120a53..561cdadbd5f 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.output.hash +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.output.hash @@ -15,4 +15,4 @@ * limitations under the License. */ -10861514367 +-12715678387 diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.sql b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.sql index a5f2d89cb23..8ff49a38a5b 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.sql +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/main/resources/kyuubi/tpch/q9.sql @@ -19,7 +19,7 @@ select nation, o_year, - sum(amount) as sum_profit + round(sum(amount), 2) as sum_profit from ( select diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala b/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala index a409a5fe927..c651d930043 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala @@ -31,18 +31,18 @@ import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSessi /** * To run this test suite: * {{{ - * KYUUBI_UPDATE=0 dev/gen/gen_tpcdh_queries.sh + * KYUUBI_UPDATE=0 dev/gen/gen_tpch_queries.sh * }}} * * To re-generate golden files for this suite: * {{{ - * dev/gen/gen_tpcdh_queries.sh + * dev/gen/gen_tpch_queries.sh * }}} */ @Slow class TPCHQuerySuite extends KyuubiFunSuite { - val queries: Set[String] = (1 to 22).map(i => s"q$i").toSet + val queries: List[String] = (1 to 22).map(i => s"q$i").toList test("run query on tiny") { val viewSuffix = "view" @@ -59,20 +59,15 @@ class TPCHQuerySuite extends KyuubiFunSuite { in.close() queryName -> queryContent }.foreach { case (name, sql) => - try { - val result = spark.sql(sql).collect() - val schema = spark.sql(sql).schema - val schemaDDL = LICENSE_HEADER + schema.toDDL + "\n" - spark.createDataFrame(result.toList.asJava, schema).createTempView(s"$name$viewSuffix") - val sumHashResult = LICENSE_HEADER + spark.sql( - s"select sum(hash(*)) from $name$viewSuffix").collect().head.get(0) + "\n" - val tuple = generateGoldenFiles("kyuubi/tpch", name, schemaDDL, sumHashResult) - assert(schemaDDL == tuple._1) - assert(sumHashResult == tuple._2) - } catch { - case cause: Throwable => - fail(name, cause) - } + val result = spark.sql(sql).collect() + val schema = spark.sql(sql).schema + val schemaDDL = LICENSE_HEADER + schema.toDDL + "\n" + spark.createDataFrame(result.toList.asJava, schema).createTempView(s"$name$viewSuffix") + val sumHashResult = LICENSE_HEADER + spark.sql( + s"select sum(hash(*)) from $name$viewSuffix").collect().head.get(0) + "\n" + val tuple = generateGoldenFiles("kyuubi/tpch", name, schemaDDL, sumHashResult) + assert(schemaDDL == tuple._1, s"query $name schema not match") + assert(sumHashResult == tuple._2, s"query $name result not match") } } } diff --git a/extensions/spark/kyuubi-spark-lineage/src/test/scala/org/apache/kyuubi/plugin/lineage/helper/SparkSQLLineageParserHelperSuite.scala b/extensions/spark/kyuubi-spark-lineage/src/test/scala/org/apache/kyuubi/plugin/lineage/helper/SparkSQLLineageParserHelperSuite.scala index 3c19163db42..76127437983 100644 --- a/extensions/spark/kyuubi-spark-lineage/src/test/scala/org/apache/kyuubi/plugin/lineage/helper/SparkSQLLineageParserHelperSuite.scala +++ b/extensions/spark/kyuubi-spark-lineage/src/test/scala/org/apache/kyuubi/plugin/lineage/helper/SparkSQLLineageParserHelperSuite.scala @@ -472,32 +472,36 @@ class SparkSQLLineageParserHelperSuite extends KyuubiFunSuite val tableDirectory = getClass.getResource("/").getPath + "table_directory" val directory = File(tableDirectory).createDirectory() val ret0 = extractLineage(s""" - |INSERT OVERWRITE DIRECTORY '$directory.path' + |INSERT OVERWRITE DIRECTORY '${directory.path}' |USING parquet |SELECT * FROM test_db0.test_table_part0""".stripMargin) assert(ret0 == Lineage( List(s"$DEFAULT_CATALOG.test_db0.test_table_part0"), - List(s"""`$directory.path`"""), + List(s"""`${directory.path}`"""), List( - (s"""`$directory.path`.key""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.key")), - (s"""`$directory.path`.value""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.value")), - (s"""`$directory.path`.pid""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.pid"))))) + (s"""`${directory.path}`.key""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.key")), + ( + s"""`${directory.path}`.value""", + Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.value")), + (s"""`${directory.path}`.pid""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.pid"))))) } test("columns lineage extract - InsertIntoHiveDirCommand") { val tableDirectory = getClass.getResource("/").getPath + "table_directory" val directory = File(tableDirectory).createDirectory() val ret0 = extractLineage(s""" - |INSERT OVERWRITE DIRECTORY '$directory.path' + |INSERT OVERWRITE DIRECTORY '${directory.path}' |USING parquet |SELECT * FROM test_db0.test_table_part0""".stripMargin) assert(ret0 == Lineage( List(s"$DEFAULT_CATALOG.test_db0.test_table_part0"), - List(s"""`$directory.path`"""), + List(s"""`${directory.path}`"""), List( - (s"""`$directory.path`.key""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.key")), - (s"""`$directory.path`.value""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.value")), - (s"""`$directory.path`.pid""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.pid"))))) + (s"""`${directory.path}`.key""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.key")), + ( + s"""`${directory.path}`.value""", + Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.value")), + (s"""`${directory.path}`.pid""", Set(s"$DEFAULT_CATALOG.test_db0.test_table_part0.pid"))))) } test("columns lineage extract - InsertIntoHiveTable") { diff --git a/externals/kyuubi-chat-engine/pom.xml b/externals/kyuubi-chat-engine/pom.xml index 3639ceed329..0633143b9fa 100644 --- a/externals/kyuubi-chat-engine/pom.xml +++ b/externals/kyuubi-chat-engine/pom.xml @@ -65,6 +65,11 @@ test + + com.squareup.retrofit2 + converter-jackson + ${retrofit.version} + diff --git a/externals/kyuubi-chat-engine/src/main/java/org/apache/kyuubi/engine/chat/ernie/enums/ChatMessageRole.java b/externals/kyuubi-chat-engine/src/main/java/org/apache/kyuubi/engine/chat/ernie/enums/ChatMessageRole.java new file mode 100644 index 00000000000..8c8921fbdf0 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/java/org/apache/kyuubi/engine/chat/ernie/enums/ChatMessageRole.java @@ -0,0 +1,41 @@ +/* + * 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.kyuubi.engine.chat.ernie.enums; + +public enum ChatMessageRole { + FUNCTION("function"), + + USER("user"), + + ASSISTANT("assistant"); + + private final String value; + + private ChatMessageRole(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + + @Override + public String toString() { + return "ChatMessageRole{" + "value='" + value + '\'' + '}'; + } +} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ApiHttpException.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ApiHttpException.scala new file mode 100644 index 00000000000..ba54f97c840 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ApiHttpException.scala @@ -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. + */ + +package org.apache.kyuubi.engine.chat.api + +class ApiHttpException(statusCode: Int, message: String, exception: Exception) + extends Exception(message, exception) {} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ErnieBotApi.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ErnieBotApi.scala new file mode 100644 index 00000000000..8593f65e51a --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/api/ErnieBotApi.scala @@ -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. + */ + +package org.apache.kyuubi.engine.chat.api + +import io.reactivex.Single +import retrofit2.http.{Body, Path, POST, Query} + +import org.apache.kyuubi.engine.chat.ernie.bean.{ChatCompletionRequest, ChatCompletionResult} + +trait ErnieBotApi { + @POST("/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}") + def createChatCompletion( + @Path("model") model: String, + @Query("access_token") accessToken: String, + @Body chatCompletionRequest: ChatCompletionRequest): Single[ChatCompletionResult] +} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionRequest.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionRequest.scala new file mode 100644 index 00000000000..6cc3a6706bf --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionRequest.scala @@ -0,0 +1,36 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import java.lang.{Double => JDouble} +import java.util.{List => JList} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class ChatCompletionRequest( + @JsonProperty("messages") messages: JList[ChatMessage], + @JsonProperty("functions") functions: JList[Function] = null, + @JsonProperty("temperature") temperature: JDouble = null, + @JsonProperty("top_p") topP: JDouble = null, + @JsonProperty("penalty_score") presenceScore: JDouble = null, + @JsonProperty("stream") stream: Boolean = false, + @JsonProperty("system") system: String = null, + @JsonProperty("stop") stop: JList[String] = null, + @JsonProperty("disable_search") disableSearch: Boolean = false, + @JsonProperty("enable_citation") enableCitation: Boolean = false, + @JsonProperty("user_id") userId: String = null) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionResult.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionResult.scala new file mode 100644 index 00000000000..e029882c5e4 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatCompletionResult.scala @@ -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 org.apache.kyuubi.engine.chat.ernie.bean + +import java.lang.{Long => JLong} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class ChatCompletionResult( + @JsonProperty("id") id: String, + @JsonProperty("object") obj: String, + @JsonProperty("created") created: JLong, + @JsonProperty("sentence_id") sentenceId: JLong, + @JsonProperty("is_end") isEnd: Boolean, + @JsonProperty("is_truncated") isTruncated: Boolean, + @JsonProperty("finish_reason") finishReason: String, + @JsonProperty("search_info") searchInfo: SearchInfo, + @JsonProperty("result") result: String, + @JsonProperty("need_clear_history") needClearHistory: Boolean, + @JsonProperty("ban_round") banRound: JLong, + @JsonProperty("usage") usage: Usage, + @JsonProperty("function_call") functionCall: FunctionCall, + @JsonProperty("error_msg") errorMsg: String, + @JsonProperty("error_code") errorCode: JLong) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatMessage.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatMessage.scala new file mode 100644 index 00000000000..d2b33222d94 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/ChatMessage.scala @@ -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. + */ + +package org.apache.kyuubi.engine.chat.ernie.bean + +import com.fasterxml.jackson.annotation.JsonProperty + +case class ChatMessage( + @JsonProperty("role") role: String, + @JsonProperty("content") content: String, + @JsonProperty("name") name: String, + @JsonProperty("function_call") functionCall: FunctionCall = null) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Example.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Example.scala new file mode 100644 index 00000000000..fe25383dec4 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Example.scala @@ -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. + */ + +package org.apache.kyuubi.engine.chat.ernie.bean + +import com.fasterxml.jackson.annotation.JsonProperty + +case class Example( + @JsonProperty("role") role: String, + @JsonProperty("name") name: String, + @JsonProperty("content") content: String = null, + @JsonProperty("function_call") functionCall: FunctionCall = null) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Function.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Function.scala new file mode 100644 index 00000000000..b0ad975bfa1 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Function.scala @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import java.util.{List => JList} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class Function( + @JsonProperty("name") name: String, + @JsonProperty("description") description: String, + @JsonProperty("parameters") parameters: Object, + @JsonProperty("responses") responses: Object = null, + @JsonProperty("examples") examples: JList[Example] = null) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/FunctionCall.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/FunctionCall.scala new file mode 100644 index 00000000000..a6b66d78f8b --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/FunctionCall.scala @@ -0,0 +1,25 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import com.fasterxml.jackson.annotation.JsonProperty + +case class FunctionCall( + @JsonProperty("name") name: String, + @JsonProperty("thoughts") thoughts: String, + @JsonProperty("arguments") arguments: String = null) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/PluginUsage.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/PluginUsage.scala new file mode 100644 index 00000000000..dd406c775a8 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/PluginUsage.scala @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import java.lang.{Long => JLong} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class PluginUsage( + @JsonProperty("name") name: String, + @JsonProperty("parse_tokens") parseTokens: JLong, + @JsonProperty("abstract_tokens") abstractTokens: JLong, + @JsonProperty("search_tokens") searchTokens: JLong, + @JsonProperty("total_tokens") totalTokens: JLong) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchInfo.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchInfo.scala new file mode 100644 index 00000000000..f97aa6c5863 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchInfo.scala @@ -0,0 +1,28 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import java.lang.{Long => JLong} +import java.util.{List => JList} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class SearchInfo( + @JsonProperty("is_beset") isBeset: JLong, + @JsonProperty("rewrite_query") rewriteQuery: String, + @JsonProperty("search_results") searchResults: JList[SearchResult]) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchResult.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchResult.scala new file mode 100644 index 00000000000..76b02be9243 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/SearchResult.scala @@ -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. + */ + +package org.apache.kyuubi.engine.chat.ernie.bean + +import com.fasterxml.jackson.annotation.JsonProperty + +case class SearchResult( + @JsonProperty("index") index: java.lang.Long, + @JsonProperty("url") url: String, + @JsonProperty("title") title: String, + @JsonProperty("datasource_id") datasourceId: String) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Usage.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Usage.scala new file mode 100644 index 00000000000..4696943be6b --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/bean/Usage.scala @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.engine.chat.ernie.bean + +import java.lang.{Long => JLong} +import java.util.{List => JList} + +import com.fasterxml.jackson.annotation.JsonProperty + +case class Usage( + @JsonProperty("prompt_tokens") promptTokens: JLong, + @JsonProperty("completion_tokens") completionTokens: JLong, + @JsonProperty("total_tokens") totalTokens: JLong, + @JsonProperty("plugins") plugins: JList[PluginUsage]) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/service/ErnieBotService.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/service/ErnieBotService.scala new file mode 100644 index 00000000000..61f56421abe --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/ernie/service/ErnieBotService.scala @@ -0,0 +1,90 @@ +/* + * 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.kyuubi.engine.chat.ernie.service + +import java.io.IOException +import java.time.Duration +import java.util.concurrent.TimeUnit + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper, PropertyNamingStrategy} +import io.reactivex.Single +import okhttp3.{ConnectionPool, OkHttpClient} +import retrofit2.{HttpException, Retrofit} +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.jackson.JacksonConverterFactory + +import org.apache.kyuubi.engine.chat.api.{ApiHttpException, ErnieBotApi} +import org.apache.kyuubi.engine.chat.ernie.bean.{ChatCompletionRequest, ChatCompletionResult} + +class ErnieBotService(api: ErnieBotApi) { + + def execute[T](apiCall: Single[T]): T = { + try apiCall.blockingGet + catch { + case httpException: HttpException => + try if (httpException.response != null && httpException.response.errorBody != null) { + val errorBody: String = httpException.response.errorBody.string + val statusCode: Int = httpException.response.code + throw new ApiHttpException(statusCode, errorBody, httpException) + } else { + throw httpException + } + catch { + case ioException: IOException => + throw httpException + } + } + } + + def createChatCompletion( + request: ChatCompletionRequest, + model: String, + accessToken: String): ChatCompletionResult = { + execute(this.api.createChatCompletion(model, accessToken, request)) + } +} + +object ErnieBotService { + final private val BASE_URL = "https://aip.baidubce.com/" + + def apply(api: ErnieBotApi): ErnieBotService = new ErnieBotService(api) + + def defaultObjectMapper: ObjectMapper = { + val mapper: ObjectMapper = new ObjectMapper + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + mapper + } + + def defaultClient(timeout: Duration): OkHttpClient = { + new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS)) + .readTimeout(timeout.toMillis, TimeUnit.MILLISECONDS) + .build + } + + def defaultRetrofit(client: OkHttpClient, mapper: ObjectMapper): Retrofit = { + new Retrofit.Builder().baseUrl(BASE_URL).client(client) + .addConverterFactory(JacksonConverterFactory.create(mapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create) + .build + } + +} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/operation/ChatOperation.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/operation/ChatOperation.scala index b0b1806f80c..60f15ea6534 100644 --- a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/operation/ChatOperation.scala +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/operation/ChatOperation.scala @@ -16,14 +16,14 @@ */ package org.apache.kyuubi.engine.chat.operation -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.engine.chat.schema.{RowSet, SchemaHelper} +import org.apache.kyuubi.engine.chat.schema.{ChatTRowSetGenerator, SchemaHelper} +import org.apache.kyuubi.engine.chat.schema.ChatTRowSetGenerator.COL_STRING_TYPE import org.apache.kyuubi.operation.{AbstractOperation, FetchIterator, OperationState} import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FETCH_PRIOR, FetchOrientation} import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ abstract class ChatOperation(session: Session) extends AbstractOperation(session) { @@ -46,8 +46,11 @@ abstract class ChatOperation(session: Session) extends AbstractOperation(session iter.fetchAbsolute(0) } - val taken = iter.take(rowSetSize) - val resultRowSet = RowSet.toTRowSet(taken.toSeq, 1, getProtocolVersion) + val taken = iter.take(rowSetSize).map(_.toSeq) + val resultRowSet = new ChatTRowSetGenerator().toTRowSet( + taken.toSeq, + Seq(COL_STRING_TYPE), + getProtocolVersion) resultRowSet.setStartRowOffset(iter.getPosition) val resp = new TFetchResultsResp(OK_STATUS) resp.setResults(resultRowSet) diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/provider/ErnieBotProvider.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/provider/ErnieBotProvider.scala new file mode 100644 index 00000000000..967ea333223 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/provider/ErnieBotProvider.scala @@ -0,0 +1,105 @@ +/* + * 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.kyuubi.engine.chat.provider + +import java.net.{InetSocketAddress, Proxy, URL} +import java.time.Duration +import java.util +import java.util.concurrent.TimeUnit + +import scala.collection.JavaConverters._ + +import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} +import com.theokanning.openai.service.OpenAiService.defaultObjectMapper + +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.engine.chat.api.ErnieBotApi +import org.apache.kyuubi.engine.chat.ernie.bean.{ChatCompletionRequest, ChatMessage} +import org.apache.kyuubi.engine.chat.ernie.enums.ChatMessageRole +import org.apache.kyuubi.engine.chat.ernie.service.ErnieBotService +import org.apache.kyuubi.engine.chat.ernie.service.ErnieBotService.{defaultClient, defaultRetrofit} + +class ErnieBotProvider(conf: KyuubiConf) extends ChatProvider { + + private val accessToken = conf.get(KyuubiConf.ENGINE_ERNIE_BOT_ACCESS_TOKEN).getOrElse { + throw new IllegalArgumentException( + s"'${KyuubiConf.ENGINE_ERNIE_BOT_ACCESS_TOKEN.key}' must be configured, " + + s"which could be got at https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5") + } + + private val model = conf.get(KyuubiConf.ENGINE_ERNIE_BOT_MODEL) + + private val ernieBotService: ErnieBotService = { + val builder = defaultClient( + Duration.ofMillis(conf.get(KyuubiConf.ENGINE_ERNIE_HTTP_SOCKET_TIMEOUT))) + .newBuilder + .connectTimeout(Duration.ofMillis(conf.get(KyuubiConf.ENGINE_ERNIE_HTTP_CONNECT_TIMEOUT))) + + conf.get(KyuubiConf.ENGINE_CHAT_GPT_HTTP_PROXY) match { + case Some(httpProxyUrl) => + val url = new URL(httpProxyUrl) + val proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(url.getHost, url.getPort)) + builder.proxy(proxy) + case _ => + } + + val retrofit = defaultRetrofit(builder.build(), defaultObjectMapper) + val ernieBotApi = retrofit.create(classOf[ErnieBotApi]) + new ErnieBotService(ernieBotApi) + } + + private var sessionUser: Option[String] = None + + private val chatHistory: LoadingCache[String, util.ArrayDeque[ChatMessage]] = + CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(new CacheLoader[String, util.ArrayDeque[ChatMessage]] { + override def load(sessionId: String): util.ArrayDeque[ChatMessage] = + new util.ArrayDeque[ChatMessage] + }) + + override def open(sessionId: String, user: Option[String]): Unit = { + sessionUser = user + chatHistory.getIfPresent(sessionId) + } + + override def ask(sessionId: String, q: String): String = { + val messages = chatHistory.get(sessionId) + try { + messages.addLast(ChatMessage(ChatMessageRole.USER.value(), q, null)) + val completionRequest = ChatCompletionRequest( + messages = messages.asScala.toList.asJava, + userId = sessionUser.orNull) + val chatCompletionResult = ernieBotService + .createChatCompletion(completionRequest, model, accessToken) + if (chatCompletionResult.errorMsg != null) { + throw new RuntimeException(chatCompletionResult.errorMsg) + } + val responseText = chatCompletionResult.result + responseText + } catch { + case e: Throwable => + messages.removeLast() + s"Chat failed. Error: ${e.getMessage}" + } + } + + override def close(sessionId: String): Unit = { + chatHistory.invalidate(sessionId) + } +} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/ChatTRowSetGenerator.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/ChatTRowSetGenerator.scala new file mode 100644 index 00000000000..7e6a121bef7 --- /dev/null +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/ChatTRowSetGenerator.scala @@ -0,0 +1,50 @@ +/* + * 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.kyuubi.engine.chat.schema + +import org.apache.kyuubi.engine.chat.schema.ChatTRowSetGenerator._ +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class ChatTRowSetGenerator + extends TRowSetGenerator[Seq[String], Seq[String], String] { + + override def getColumnSizeFromSchemaType(schema: Seq[String]): Int = schema.length + + override def getColumnType(schema: Seq[String], ordinal: Int): String = COL_STRING_TYPE + + override def isColumnNullAt(row: Seq[String], ordinal: Int): Boolean = row(ordinal) == null + + override def getColumnAs[T](row: Seq[String], ordinal: Int): T = row(ordinal).asInstanceOf[T] + + override def toTColumn(rows: Seq[Seq[String]], ordinal: Int, typ: String): TColumn = + typ match { + case COL_STRING_TYPE => asStringTColumn(rows, ordinal) + case otherType => throw new UnsupportedOperationException(s"type $otherType") + } + + override def toTColumnValue(row: Seq[String], ordinal: Int, types: Seq[String]): TColumnValue = + getColumnType(types, ordinal) match { + case COL_STRING_TYPE => asStringTColumnValue(row, ordinal) + case otherType => throw new UnsupportedOperationException(s"type $otherType") + } +} + +object ChatTRowSetGenerator { + val COL_STRING_TYPE: String = classOf[String].getSimpleName +} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/RowSet.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/RowSet.scala deleted file mode 100644 index 3bb4ba7dfa9..00000000000 --- a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/RowSet.scala +++ /dev/null @@ -1,108 +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. - */ - -package org.apache.kyuubi.engine.chat.schema - -import java.util - -import org.apache.hive.service.rpc.thrift._ - -import org.apache.kyuubi.util.RowSetUtils._ - -object RowSet { - - def emptyTRowSet(): TRowSet = { - new TRowSet(0, new java.util.ArrayList[TRow](0)) - } - - def toTRowSet( - rows: Seq[Array[String]], - columnSize: Int, - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBasedSet(rows, columnSize) - } else { - toColumnBasedSet(rows, columnSize) - } - } - - def toRowBasedSet(rows: Seq[Array[String]], columnSize: Int): TRowSet = { - val rowSize = rows.length - val tRows = new java.util.ArrayList[TRow](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - var j = 0 - val columnSize = row.length - while (j < columnSize) { - val columnValue = stringTColumnValue(j, row) - tRow.addToColVals(columnValue) - j += 1 - } - i += 1 - tRows.add(tRow) - } - new TRowSet(0, tRows) - } - - def toColumnBasedSet(rows: Seq[Array[String]], columnSize: Int): TRowSet = { - val rowSize = rows.length - val tRowSet = new TRowSet(0, new util.ArrayList[TRow](rowSize)) - var i = 0 - while (i < columnSize) { - val tColumn = toTColumn(rows, i) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - private def toTColumn(rows: Seq[Array[String]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[String](rows, ordinal, nulls, "") - TColumn.stringVal(new TStringColumn(values, nulls)) - } - - private def getOrSetAsNull[String]( - rows: Seq[Array[String]], - ordinal: Int, - nulls: util.BitSet, - defaultVal: String): util.List[String] = { - val size = rows.length - val ret = new util.ArrayList[String](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row(ordinal) == null - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row(ordinal)) - } - idx += 1 - } - ret - } - - private def stringTColumnValue(ordinal: Int, row: Array[String]): TColumnValue = { - val tStringValue = new TStringValue - if (row(ordinal) != null) tStringValue.setValue(row(ordinal)) - TColumnValue.stringVal(tStringValue) - } -} diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/SchemaHelper.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/SchemaHelper.scala index 8ccfdda2fe9..2b380f3845d 100644 --- a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/SchemaHelper.scala +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/schema/SchemaHelper.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.engine.chat.schema import java.util.Collections -import org.apache.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ object SchemaHelper { diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionImpl.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionImpl.scala index 6ec6d062600..0d836877445 100644 --- a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionImpl.scala +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionImpl.scala @@ -16,11 +16,10 @@ */ package org.apache.kyuubi.engine.chat.session -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} - import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiSQLException} import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.session.{AbstractSession, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} class ChatSessionImpl( protocol: TProtocolVersion, diff --git a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionManager.scala b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionManager.scala index 33a9dd45066..ff5c4748e34 100644 --- a/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionManager.scala +++ b/externals/kyuubi-chat-engine/src/main/scala/org/apache/kyuubi/engine/chat/session/ChatSessionManager.scala @@ -16,8 +16,6 @@ */ package org.apache.kyuubi.engine.chat.session -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.ENGINE_SHARE_LEVEL import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY @@ -27,6 +25,7 @@ import org.apache.kyuubi.engine.chat.operation.ChatOperationManager import org.apache.kyuubi.engine.chat.provider.ChatProvider import org.apache.kyuubi.operation.OperationManager import org.apache.kyuubi.session.{Session, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class ChatSessionManager(name: String) extends SessionManager(name) { diff --git a/externals/kyuubi-flink-sql-engine/pom.xml b/externals/kyuubi-flink-sql-engine/pom.xml index eec5c1cd9e8..d01f05fed7d 100644 --- a/externals/kyuubi-flink-sql-engine/pom.xml +++ b/externals/kyuubi-flink-sql-engine/pom.xml @@ -105,6 +105,12 @@ provided + + org.apache.flink + flink-table-planner-loader + provided + + org.apache.kyuubi @@ -180,8 +186,6 @@ com.google.guava:guava commons-codec:commons-codec org.apache.commons:commons-lang3 - org.apache.hive:hive-service-rpc - org.apache.thrift:* org.apache.kyuubi:* @@ -233,27 +237,6 @@ org.apache.commons.lang ${kyuubi.shade.packageName}.org.apache.commons.lang - - org.apache.hive.service.rpc.thrift - ${kyuubi.shade.packageName}.org.apache.hive.service.rpc.thrift - - org.apache.hive.service.rpc.thrift.** - - - - com.facebook.fb303 - ${kyuubi.shade.packageName}.com.facebook.fb303 - - com.facebook.fb303.** - - - - org.apache.thrift - ${kyuubi.shade.packageName}.org.apache.thrift - - org.apache.thrift.** - - diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/FlinkSQLEngine.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/FlinkSQLEngine.scala index 8838799bc24..dff9aa6025b 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/FlinkSQLEngine.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/FlinkSQLEngine.scala @@ -32,6 +32,7 @@ import org.apache.flink.table.gateway.service.context.DefaultContext import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.Utils.{addShutdownHook, currentUser, FLINK_ENGINE_SHUTDOWN_PRIORITY} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.ENGINE_FLINK_INITIALIZE_SQL import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_ENGINE_NAME, KYUUBI_SESSION_USER_KEY} import org.apache.kyuubi.engine.flink.FlinkSQLEngine.{countDownLatch, currentEngine} import org.apache.kyuubi.service.Serverable @@ -102,9 +103,7 @@ object FlinkSQLEngine extends Logging { startEngine(engineContext) info("Flink engine started") - if ("yarn-application".equalsIgnoreCase(executionTarget)) { - bootstrapFlinkApplicationExecutor() - } + bootstrap(executionTarget) // blocking main thread countDownLatch.await() @@ -129,15 +128,22 @@ object FlinkSQLEngine extends Logging { } } - private def bootstrapFlinkApplicationExecutor() = { - // trigger an execution to initiate EmbeddedExecutor with the default flink conf + private def bootstrap(executionTarget: String) = { val flinkConf = new Configuration() - flinkConf.set(PipelineOptions.NAME, "kyuubi-bootstrap-sql") - debug(s"Running bootstrap Flink SQL in application mode with flink conf: $flinkConf.") val tableEnv = TableEnvironment.create(flinkConf) - val res = tableEnv.executeSql("select 'kyuubi'") - res.await() - info("Bootstrap Flink SQL finished.") + + if ("yarn-application".equalsIgnoreCase(executionTarget)) { + // trigger an execution to initiate EmbeddedExecutor with the default flink conf + flinkConf.set(PipelineOptions.NAME, "kyuubi-bootstrap-sql") + debug(s"Running bootstrap Flink SQL in application mode with flink conf: $flinkConf.") + tableEnv.executeSql("select 'kyuubi'").await() + } + + kyuubiConf.get(ENGINE_FLINK_INITIALIZE_SQL).foreach { stmt => + tableEnv.executeSql(stmt).await() + } + + info("Bootstrap SQL finished.") } private def setDeploymentConf(executionTarget: String, flinkConf: Configuration): Unit = { diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/ExecuteStatement.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/ExecuteStatement.scala index 0e0c476e2d4..f30b6ab8627 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/ExecuteStatement.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/ExecuteStatement.scala @@ -17,10 +17,15 @@ package org.apache.kyuubi.engine.flink.operation +import java.util.Optional + import scala.concurrent.duration.Duration import org.apache.flink.api.common.JobID +import org.apache.flink.table.api.TableException import org.apache.flink.table.gateway.api.operation.OperationHandle +import org.apache.flink.table.operations.Operation +import org.apache.flink.table.operations.command.HelpOperation import org.apache.kyuubi.Logging import org.apache.kyuubi.engine.flink.FlinkEngineUtils @@ -28,6 +33,7 @@ import org.apache.kyuubi.engine.flink.result.ResultSetUtil import org.apache.kyuubi.operation.OperationState import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.util.reflect.{DynConstructors, DynFields, DynMethods} class ExecuteStatement( session: Session, @@ -59,6 +65,14 @@ class ExecuteStatement( private def executeStatement(): Unit = { try { setState(OperationState.RUNNING) + + val operation = parseExtendedStatement(statement) + if (operation.isPresent && operation.get().isInstanceOf[HelpOperation]) { + resultSet = ResultSetUtil.helpMessageResultSet + setState(OperationState.FINISHED) + return + } + val resultFetcher = executor.executeStatement( new OperationHandle(getHandle.identifier), statement) @@ -71,4 +85,36 @@ class ExecuteStatement( shutdownTimeoutMonitor() } } + + private def parseExtendedStatement(statement: String): Optional[Operation] = { + val plannerModuleClassLoader: ClassLoader = getPlannerModuleClassLoader + val extendedParser: AnyRef = + DynConstructors.builder() + .loader(plannerModuleClassLoader) + .impl("org.apache.flink.table.planner.parse.ExtendedParser") + .build().newInstance() + DynMethods.builder("parse") + .hiddenImpl(extendedParser.getClass, classOf[String]) + .buildChecked() + .invoke(extendedParser, statement) + } + + private def getPlannerModuleClassLoader: ClassLoader = { + try { + val plannerModule = DynMethods.builder("getInstance") + .hiddenImpl("org.apache.flink.table.planner.loader.PlannerModule") + .buildStaticChecked() + .invoke().asInstanceOf[AnyRef] + + DynFields.builder() + .hiddenImpl(plannerModule.getClass, "submoduleClassLoader") + .build[ClassLoader].bind(plannerModule).get + } catch { + case e: Exception => + throw new TableException( + "Error obtaining Flink planner module ClassLoader. " + + "Make sure a flink-table-planner-loader.jar is on the classpath", + e) + } + } } diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperation.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperation.scala index 1424b721c4b..df067a888c6 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperation.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperation.scala @@ -28,16 +28,16 @@ import org.apache.flink.configuration.Configuration import org.apache.flink.table.gateway.service.context.SessionContext import org.apache.flink.table.gateway.service.operation.OperationExecutor import org.apache.flink.types.Row -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TTableSchema} import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.engine.flink.result.ResultSet -import org.apache.kyuubi.engine.flink.schema.RowSet +import org.apache.kyuubi.engine.flink.schema.{FlinkTRowSetGenerator, RowSet} import org.apache.kyuubi.engine.flink.session.FlinkSessionImpl import org.apache.kyuubi.operation.{AbstractOperation, OperationState} import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FETCH_PRIOR, FetchOrientation} import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TTableSchema} abstract class FlinkOperation(session: Session) extends AbstractOperation(session) { @@ -133,10 +133,9 @@ abstract class FlinkOperation(session: Session) extends AbstractOperation(sessio case Some(tz) => ZoneId.of(tz) case None => ZoneId.systemDefault() } - val resultRowSet = RowSet.resultSetToTRowSet( + val resultRowSet = new FlinkTRowSetGenerator(zoneId).toTRowSet( batch.toList, resultSet, - zoneId, getProtocolVersion) val resp = new TFetchResultsResp(OK_STATUS) resp.setResults(resultRowSet) diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkSQLOperationManager.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkSQLOperationManager.scala index d5c0629eedd..324efb6585c 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkSQLOperationManager.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/operation/FlinkSQLOperationManager.scala @@ -37,6 +37,9 @@ class FlinkSQLOperationManager extends OperationManager("FlinkSQLOperationManage private lazy val resultMaxRowsDefault = getConf.get(ENGINE_FLINK_MAX_ROWS) + private lazy val resultFetchTimeoutDefault = getConf.get(ENGINE_FLINK_FETCH_TIMEOUT) + .map(_ milliseconds).getOrElse(Duration.Inf) + private lazy val operationConvertCatalogDatabaseDefault = getConf.get(ENGINE_OPERATION_CONVERT_CATALOG_DATABASE_ENABLED) @@ -70,8 +73,11 @@ class FlinkSQLOperationManager extends OperationManager("FlinkSQLOperationManage resultMaxRowsDefault.toString).toInt val resultFetchTimeout = - flinkSession.normalizedConf.get(ENGINE_FLINK_FETCH_TIMEOUT.key).map(_.toLong milliseconds) - .getOrElse(Duration.Inf) + flinkSession.normalizedConf + .get(ENGINE_FLINK_FETCH_TIMEOUT.key) + .map(ENGINE_FLINK_FETCH_TIMEOUT.valueConverter) + .map(_.get milliseconds) + .getOrElse(resultFetchTimeoutDefault) val op = mode match { case NoneMode => diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/CommandStrings.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/CommandStrings.scala new file mode 100644 index 00000000000..56a199fa697 --- /dev/null +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/CommandStrings.scala @@ -0,0 +1,245 @@ +/* + * 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.kyuubi.engine.flink.result + +import scala.collection.mutable.ListBuffer + +import org.apache.flink.util.Preconditions +import org.jline.utils.{AttributedString, AttributedStringBuilder, AttributedStyle} + +/** + * Utility class that contains all strings for Flink SQL commands and messages. + */ +object CommandStrings { + private val CMD_DESC_DELIMITER = "\t\t" + + private class SQLCommandsDescriptions { + private var commandMaxLength = -1 + private val commandsDescriptionList = ListBuffer[(String, String)]() + + def commandDescription(command: String, description: String): SQLCommandsDescriptions = { + Preconditions.checkState( + command.nonEmpty, + s"content of command must not be empty.", + Seq(): _*) + Preconditions.checkState( + description.nonEmpty, + s"content of command's description must not be empty.", + Seq(): _*) + + updateMaxCommandLength(command.length) + commandsDescriptionList += ((command, description)) + this + } + + private def updateMaxCommandLength(newLength: Int): Unit = { + Preconditions.checkState(newLength > 0) + if (commandMaxLength < newLength) { + commandMaxLength = newLength + } + } + + private def formatDescription(input: String): String = { + val maxLineLength = 160 + val newLinePrefix = " " * commandMaxLength + CMD_DESC_DELIMITER + val words = input.split(" ") + + val (lastLine, lines) = words.foldLeft(("", List[String]())) { + case ((line, lines), word) => + val newLine = if (line.isEmpty) word else line + " " + word + if (newLine.length > maxLineLength) (word, lines :+ line) else (newLine, lines) + } + + (lines :+ lastLine).mkString("\n" + newLinePrefix) + } + + def build(): AttributedString = { + val attributedStringBuilder = new AttributedStringBuilder + if (commandsDescriptionList.nonEmpty) { + commandsDescriptionList.foreach { + case (cmd, cmdDesc) => + attributedStringBuilder + .style(AttributedStyle.DEFAULT.bold()) + .append(cmd.padTo(commandMaxLength, " ").mkString) + .append(CMD_DESC_DELIMITER) + .style(AttributedStyle.DEFAULT) + .append(formatDescription(cmdDesc)) + .append('\n') + } + } + attributedStringBuilder.toAttributedString + } + } + + // scalastyle:off line.size.limit + val MESSAGE_HELP: AttributedString = + new AttributedStringBuilder() + .append("The following commands are available:\n\n") + .append(COMMANDS_DESCRIPTIONS) + .style(AttributedStyle.DEFAULT.underline()) + .append("\nHint") + .style(AttributedStyle.DEFAULT) + .append( + ": Make sure that a statement ends with \";\" for finalizing (multi-line) statements.") + // About Documentation Link. + .style(AttributedStyle.DEFAULT) + .append( + "\nThe above list includes only the most frequently used statements.\nYou can also type any Flink SQL statement, please visit https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/sql/overview/ for more details.") + .toAttributedString + + def COMMANDS_DESCRIPTIONS: AttributedString = + new SQLCommandsDescriptions() + .commandDescription( + "HELP", + "Prints the available commands or the detailed description of a specified command.") + .commandDescription( + "SET", + "Sets a session configuration property. Syntax: \"SET ''='';\". Use \"SET;\" for listing all properties.") + .commandDescription( + "RESET", + "Resets a session configuration property. Syntax: \"RESET '';\". Use \"RESET;\" for reset all session properties.") + .commandDescription( + "INSERT INTO", + "Inserts the results of a SQL SELECT query into a declared table sink.") + .commandDescription( + "INSERT OVERWRITE", + "Inserts the results of a SQL SELECT query into a declared table sink and overwrite existing data.") + .commandDescription( + "SELECT", + "Executes a SQL SELECT query on the Flink cluster.") + .commandDescription( + "EXPLAIN", + "Describes the execution plan of a query or table with the given name.") + .commandDescription( + "BEGIN STATEMENT SET", + "Begins a statement set. Syntax: \"BEGIN STATEMENT SET;\"") + .commandDescription("END", "Ends a statement set. Syntax: \"END;\"") + .commandDescription( + "ADD JAR", + "Adds the specified jar file to the submitted jobs' classloader. Syntax: \"ADD JAR '.jar'\"") + .commandDescription( + "SHOW JARS", + "Shows the list of user-specified jar dependencies. This list is impacted by the ADD JAR commands.") + .commandDescription( + "SHOW CATALOGS", + "Shows the list of registered catalogs.") + .commandDescription( + "SHOW CURRENT CATALOG", + "Shows the name of the current catalog.") + .commandDescription( + "SHOW DATABASES", + "Shows all databases in the current catalog.") + .commandDescription( + "SHOW CURRENT DATABASE", + "Shows the name of the current database.") + .commandDescription( + "SHOW TABLES", + "Shows all tables for an optionally specified database. Syntax: \"SHOW TABLES [ ( FROM | IN ) [catalog_name.]database_name ] [ [NOT] LIKE ]\"") + .commandDescription( + "SHOW CREATE TABLE", + "Shows the CREATE TABLE statement that creates the specified table.") + .commandDescription( + "SHOW COLUMNS", + "Shows all columns of a table with the given name. Syntax: \"SHOW COLUMNS ( FROM | IN ) [[catalog_name.]database.] [ [NOT] LIKE ]\"") + .commandDescription( + "SHOW VIEWS", + "Shows all views in the current catalog and the current database.") + .commandDescription( + "SHOW CREATE VIEW", + "Shows the CREATE VIEW statement that creates the specified view. Syntax: \"SHOW CREATE VIEW [catalog_name.][db_name.]view_name\"") + .commandDescription( + "SHOW FUNCTIONS", + "Shows all user-defined and built-in functions in the current catalog and current database. Use \"SHOW USER FUNCTIONS\" for listing all user-defined functions in the current catalog and current database.") + .commandDescription( + "SHOW MODULES", + "Shows all enabled module names with resolution order.") + .commandDescription( + "USE CATALOG", + "Sets the current catalog. All subsequent commands that do not explicitly specify a catalog will use this one. If the provided catalog does not exist, an exception is thrown. The default current catalog is default_catalog. Syntax: \"USE CATALOG catalog_name\"") + .commandDescription( + "USE", + "Sets the current database. All subsequent commands that do not explicitly specify a database will use this one. If the provided database does not exist, an exception is thrown. The default current database is default_database. Syntax: \"USE [catalog_name.]database_name\"") + .commandDescription( + "DESC", + "Describes the schema of a table with the given name. Syntax: \"{ DESCRIBE | DESC } [catalog_name.][db_name.]table_name\"") + .commandDescription( + "ANALYZE", + "ANALYZE statements are used to collect statistics for existing tables and store the result to catalog. Only supports in batch mode. Syntax: \"ANALYZE TABLE [catalog_name.][db_name.]table_name PARTITION(partcol1[=val1] [, partcol2[=val2], ...]) COMPUTE STATISTICS [FOR COLUMNS col1 [, col2, ...] | FOR ALL COLUMNS]\"") + .commandDescription( + "ALTER TABLE", + "Renames a table or change a table's properties. Syntax: \"ALTER TABLE [catalog_name.][db_name.]table_name RENAME TO new_table_name\", the other syntax: \"ALTER TABLE [catalog_name.][db_name.]table_name SET ( key1=val1[, key2=val2, ...] )\"") + .commandDescription( + "ALTER VIEW", + "Renames a given view to a new name within the same catalog and database. Syntax: \"ALTER VIEW [catalog_name.][db_name.]view_name RENAME TO new_view_name\"") + .commandDescription( + "ALTER DATABASE", + "Changes a database's properties. Syntax: \"ALTER DATABASE [catalog_name.]db_name SET ( key1=val1[, key2=val2, ...] )\"") + .commandDescription( + "ALTER FUNCTION", + "Changes a catalog function with the new identifier and optional language tag. Syntax: \"ALTER [TEMPORARY|TEMPORARY SYSTEM] FUNCTION [IF EXISTS] [catalog_name.][db_name.]function_name AS identifier [LANGUAGE JAVA|SCALA|PYTHON]\"") + .commandDescription( + "DROP CATALOG", + "Drops a catalog with the given catalog name. Syntax: \"DROP CATALOG [IF EXISTS] catalog_name\"") + .commandDescription( + "DROP DATABASE", + "Drops a database with the given database name. Syntax: \"DROP DATABASE [IF EXISTS] [catalog_name.]db_name [ (RESTRICT | CASCADE) ]\"") + .commandDescription( + "DROP TABLE", + "Drops a table with the given table name. Syntax: \"DROP [TEMPORARY] TABLE [IF EXISTS] [catalog_name.][db_name.]table_name\"") + .commandDescription( + "DROP VIEW", + "Drops a view with the given view name. Syntax: \"DROP [TEMPORARY] VIEW [IF EXISTS] [catalog_name.][db_name.]view_name\"") + .commandDescription( + "DROP FUNCTION", + "Drops a catalog function with the given function name. Syntax: \"DROP [TEMPORARY|TEMPORARY SYSTEM] FUNCTION [IF EXISTS] [catalog_name.][db_name.]function_name\"") + .commandDescription( + "CREATE CATALOG", + "Creates a catalog with the given catalog properties. Syntax: \"CREATE CATALOG catalog_name WITH ( 'key1'='value1'[, 'key2'='value2', ...] )\"") + .commandDescription( + "CREATE DATABASE", + "Creates a database with the given database properties. Syntax: \"CREATE DATABASE [IF NOT EXISTS] [catalog_name.]db_name [COMMENT 'database_comment'] [WITH ( 'key1'='value1'[, 'key2'='value2', ...] )]\"") + .commandDescription( + "CREATE TABLE", + "Creates a table with the given table properties. Syntax: \"CREATE [TEMPORARY] TABLE [IF NOT EXISTS] [catalog_name.][db_name.]table_name ( { col_name data_type [COMMENT col_comment] [column_constraint] | table_constraint } [,...] ) [COMMENT table_comment] [PARTITIONED BY (col_name, col_name, ...)] [WITH ( 'key1'='value1'[, 'key2'='value2', ...] )] \"") + .commandDescription( + "CREATE VIEW", + "Creates a view with the given view expression. Syntax: \"CREATE [TEMPORARY] VIEW [IF NOT EXISTS] [catalog_name.][db_name.]view_name [(column_name [,...])] [COMMENT view_comment] AS query_expression\"") + .commandDescription( + "CREATE FUNCTION", + "Creates a catalog function with the given function properties. Syntax: \"CREATE [TEMPORARY|TEMPORARY SYSTEM] FUNCTION [IF NOT EXISTS] [catalog_name.][db_name.]function_name AS identifier [LANGUAGE JAVA|SCALA|PYTHON] [USING JAR '.jar' [, JAR '.jar']* ]\"") + .commandDescription( + "SHOW JOBS", + "Show the jobs in the Flink cluster. Supports in version 1.17 and later.") + .commandDescription( + "STOP JOB", + "Stop the job with the given job ID. Supports in version 1.17 and later. Syntax: \"STOP JOB '' [WITH SAVEPOINT] [WITH DRAIN]\"") + .commandDescription( + "UPDATE", + "Performs row-level updating on the target table. Only supports in batch mode. Supports in version 1.17 and later. Syntax: \"UPDATE [catalog_name.][db_name.]table_name SET col_name1 = col_val1 [, col_name2 = col_val2 ...] [WHERE condition]\"") + .commandDescription( + "DELETE", + "Performs row-level deleting on the target table. Only supports in batch mode. Supports in version 1.17 and later. Syntax: \"DELETE FROM [catalog_name.][db_name.]table_name [WHERE condition]\"") + .commandDescription( + "TRUNCATE TABLE", + "Truncates the target table. Only supports in batch mode. Supports in version 1.18 and later. Syntax: \"TRUNCATE TABLE [catalog_name.][db_name.]table_name\"") + .commandDescription( + "CALL", + "Calls a stored procedure. Supports in version 1.18 and later. Syntax: \"CALL [catalog_name.][database_name.]procedure_name ([ expression [, expression]* ] )\"") + .build() + // scalastyle:on +} diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/QueryResultFetchIterator.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/IncrementalResultFetchIterator.scala similarity index 88% rename from externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/QueryResultFetchIterator.scala rename to externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/IncrementalResultFetchIterator.scala index 60ae08d9dd8..60c92d9afdf 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/QueryResultFetchIterator.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/IncrementalResultFetchIterator.scala @@ -34,10 +34,12 @@ import org.apache.flink.table.types.DataType import org.apache.flink.types.Row import org.apache.kyuubi.Logging +import org.apache.kyuubi.engine.flink.FlinkEngineUtils import org.apache.kyuubi.engine.flink.shim.FlinkResultSet import org.apache.kyuubi.operation.FetchIterator +import org.apache.kyuubi.util.reflect.DynFields -class QueryResultFetchIterator( +class IncrementalResultFetchIterator( resultFetcher: ResultFetcher, maxRows: Int = 1000000, resultFetchTimeout: Duration = Duration.Inf) extends FetchIterator[Row] with Logging { @@ -58,8 +60,17 @@ class QueryResultFetchIterator( val FETCH_INTERVAL_MS: Long = 1000 + // for Flink 1.16 and below, isQueryResult is not supported + val isQueryResult: Boolean = + FlinkEngineUtils.FLINK_RUNTIME_VERSION < "1.17" || + DynFields.builder + .hiddenImpl(classOf[ResultFetcher], "isQueryResult") + .build[Boolean](resultFetcher).get() + + val effectiveMaxRows: Int = if (isQueryResult) maxRows else Int.MaxValue + private val executor = Executors.newSingleThreadScheduledExecutor( - new ThreadFactoryBuilder().setNameFormat("flink-query-iterator-%d").setDaemon(true).build) + new ThreadFactoryBuilder().setNameFormat("flink-result-iterator-%d").setDaemon(true).build) implicit private val executionContext: ExecutionContextExecutor = ExecutionContext.fromExecutor(executor) @@ -78,7 +89,7 @@ class QueryResultFetchIterator( // if no timeout is set, this would block until some rows are fetched debug(s"Fetching from result store with timeout $resultFetchTimeout ms") while (!fetched && !Thread.interrupted()) { - val rs = resultFetcher.fetchResults(token, maxRows - bufferedRows.length) + val rs = resultFetcher.fetchResults(token, effectiveMaxRows - bufferedRows.length) val flinkRs = new FlinkResultSet(rs) // TODO: replace string-based match when Flink 1.16 support is dropped flinkRs.getResultType.name() match { @@ -144,7 +155,7 @@ class QueryResultFetchIterator( debug(s"Fetching from buffered rows at pos $pos.") val row = bufferedRows(pos.toInt) pos += 1 - if (pos >= maxRows) { + if (pos >= effectiveMaxRows) { hasNext = false } row @@ -154,7 +165,7 @@ class QueryResultFetchIterator( if (hasNext) { val row = bufferedRows(pos.toInt) pos += 1 - if (pos >= maxRows) { + if (pos >= effectiveMaxRows) { hasNext = false } row diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSet.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSet.scala index b8d407297ac..f9d3de0ab97 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSet.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSet.scala @@ -53,7 +53,7 @@ case class ResultSet( def close: Unit = { data match { - case queryIte: QueryResultFetchIterator => queryIte.close() + case incIte: IncrementalResultFetchIterator => incIte.close() case _ => } } diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSetUtil.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSetUtil.scala index 8b722f1e5e9..032c86ac13f 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSetUtil.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/result/ResultSetUtil.scala @@ -58,6 +58,13 @@ object ResultSetUtil { .data(Array[Row](Row.of("OK"))) .build + def helpMessageResultSet: ResultSet = + ResultSet.builder + .resultKind(ResultKind.SUCCESS_WITH_CONTENT) + .columns(Column.physical("result", DataTypes.STRING)) + .data(Array[Row](Row.of(CommandStrings.MESSAGE_HELP.toString))) + .build + def fromResultFetcher( resultFetcher: ResultFetcher, maxRows: Int, @@ -66,7 +73,7 @@ object ResultSetUtil { throw new IllegalArgumentException("maxRows should be positive") } val schema = resultFetcher.getResultSchema - val ite = new QueryResultFetchIterator(resultFetcher, maxRows, resultFetchTimeout) + val ite = new IncrementalResultFetchIterator(resultFetcher, maxRows, resultFetchTimeout) ResultSet.builder .resultKind(ResultKind.SUCCESS_WITH_CONTENT) .columns(schema.getColumns) diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/FlinkTRowSetGenerator.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/FlinkTRowSetGenerator.scala new file mode 100644 index 00000000000..463b66111a5 --- /dev/null +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/FlinkTRowSetGenerator.scala @@ -0,0 +1,112 @@ +/* + * 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.kyuubi.engine.flink.schema + +import java.time.{Instant, ZonedDateTime, ZoneId} + +import org.apache.flink.table.data.StringData +import org.apache.flink.table.types.logical._ +import org.apache.flink.types.Row + +import org.apache.kyuubi.engine.flink.result.ResultSet +import org.apache.kyuubi.engine.flink.schema.RowSet.{toHiveString, TIMESTAMP_LZT_FORMATTER} +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class FlinkTRowSetGenerator(zoneId: ZoneId) + extends TRowSetGenerator[ResultSet, Row, LogicalType] { + override def getColumnSizeFromSchemaType(schema: ResultSet): Int = schema.columns.size + + override def getColumnType(schema: ResultSet, ordinal: Int): LogicalType = + schema.columns.get(ordinal).getDataType.getLogicalType + + override def isColumnNullAt(row: Row, ordinal: Int): Boolean = row.getField(ordinal) == null + + override def getColumnAs[T](row: Row, ordinal: Int): T = row.getFieldAs[T](ordinal) + + override def toTColumnValue(row: Row, ordinal: Int, types: ResultSet): TColumnValue = { + getColumnType(types, ordinal) match { + case _: BooleanType => asBooleanTColumnValue(row, ordinal) + case _: TinyIntType => asByteTColumnValue(row, ordinal) + case _: SmallIntType => asShortTColumnValue(row, ordinal) + case _: IntType => asIntegerTColumnValue(row, ordinal) + case _: BigIntType => asLongTColumnValue(row, ordinal) + case _: DoubleType => asDoubleTColumnValue(row, ordinal) + case _: FloatType => asFloatTColumnValue(row, ordinal) + case t @ (_: VarCharType | _: CharType) => + asStringTColumnValue( + row, + ordinal, + convertFunc = { + case value: String => value + case value: StringData => value.toString + case null => null + case other => throw new IllegalArgumentException( + s"Unsupported conversion class ${other.getClass} for type ${t.getClass}.") + }) + case _: LocalZonedTimestampType => + asStringTColumnValue( + row, + ordinal, + rawValue => + TIMESTAMP_LZT_FORMATTER.format( + ZonedDateTime.ofInstant(rawValue.asInstanceOf[Instant], zoneId))) + case t => asStringTColumnValue(row, ordinal, rawValue => toHiveString((rawValue, t))) + } + } + + override def toTColumn(rows: Seq[Row], ordinal: Int, logicalType: LogicalType): TColumn = { + // for each column, determine the conversion class by sampling the first non-value value + // if there's no row, set the entire column empty + logicalType match { + case _: BooleanType => asBooleanTColumn(rows, ordinal) + case _: TinyIntType => asByteTColumn(rows, ordinal) + case _: SmallIntType => asShortTColumn(rows, ordinal) + case _: IntType => asIntegerTColumn(rows, ordinal) + case _: BigIntType => asLongTColumn(rows, ordinal) + case _: FloatType => asFloatTColumn(rows, ordinal) + case _: DoubleType => asDoubleTColumn(rows, ordinal) + case t @ (_: VarCharType | _: CharType) => + val sampleField = rows.iterator.map(_.getField(ordinal)).find(_ ne null).orNull + sampleField match { + case _: String => asStringTColumn(rows, ordinal) + case _: StringData => + asStringTColumn( + rows, + ordinal, + convertFunc = (row, ordinal) => getColumnAs[StringData](row, ordinal).toString) + case null => asStringTColumn(rows, ordinal) + case other => throw new IllegalArgumentException( + s"Unsupported conversion class ${other.getClass} for type ${t.getClass}.") + } + case _: LocalZonedTimestampType => + asStringTColumn( + rows, + ordinal, + TIMESTAMP_LZT_FORMATTER.format(ZonedDateTime.ofInstant(Instant.EPOCH, zoneId)), + (row, ordinal) => + TIMESTAMP_LZT_FORMATTER.format( + ZonedDateTime.ofInstant(getColumnAs[Instant](row, ordinal), zoneId))) + case _ => + asStringTColumn( + rows, + ordinal, + convertFunc = (row, ordinal) => toHiveString((row.getField(ordinal), logicalType))) + } + } + +} diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/RowSet.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/RowSet.scala index c446396d5bb..7015d7c52b6 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/RowSet.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/schema/RowSet.scala @@ -17,262 +17,25 @@ package org.apache.kyuubi.engine.flink.schema -import java.{lang, util} -import java.nio.ByteBuffer import java.nio.charset.StandardCharsets import java.sql.{Date, Timestamp} -import java.time.{Instant, LocalDate, LocalDateTime, ZonedDateTime, ZoneId} +import java.time.{LocalDate, LocalDateTime} import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, TextStyle} import java.time.temporal.ChronoField import java.util.Collections import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer -import scala.language.implicitConversions import org.apache.flink.table.catalog.Column -import org.apache.flink.table.data.StringData import org.apache.flink.table.types.logical._ import org.apache.flink.types.Row -import org.apache.hive.service.rpc.thrift._ -import org.apache.kyuubi.engine.flink.result.ResultSet +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ import org.apache.kyuubi.util.RowSetUtils._ object RowSet { - def resultSetToTRowSet( - rows: Seq[Row], - resultSet: ResultSet, - zoneId: ZoneId, - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBaseSet(rows, resultSet, zoneId) - } else { - toColumnBasedSet(rows, resultSet, zoneId) - } - } - - def toRowBaseSet(rows: Seq[Row], resultSet: ResultSet, zoneId: ZoneId): TRowSet = { - val rowSize = rows.size - val tRows = new util.ArrayList[TRow](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - val columnSize = row.getArity - var j = 0 - while (j < columnSize) { - val columnValue = toTColumnValue(j, row, resultSet, zoneId) - tRow.addToColVals(columnValue) - j += 1 - } - tRows.add(tRow) - i += 1 - } - - new TRowSet(0, tRows) - } - - def toColumnBasedSet(rows: Seq[Row], resultSet: ResultSet, zoneId: ZoneId): TRowSet = { - val size = rows.length - val tRowSet = new TRowSet(0, new util.ArrayList[TRow](size)) - val columnSize = resultSet.getColumns.size() - var i = 0 - while (i < columnSize) { - val field = resultSet.getColumns.get(i) - val tColumn = toTColumn(rows, i, field.getDataType.getLogicalType, zoneId) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - private def toTColumnValue( - ordinal: Int, - row: Row, - resultSet: ResultSet, - zoneId: ZoneId): TColumnValue = { - - val column = resultSet.getColumns.get(ordinal) - val logicalType = column.getDataType.getLogicalType - - logicalType match { - case _: BooleanType => - val boolValue = new TBoolValue - if (row.getField(ordinal) != null) { - boolValue.setValue(row.getField(ordinal).asInstanceOf[Boolean]) - } - TColumnValue.boolVal(boolValue) - case _: TinyIntType => - val tByteValue = new TByteValue - if (row.getField(ordinal) != null) { - tByteValue.setValue(row.getField(ordinal).asInstanceOf[Byte]) - } - TColumnValue.byteVal(tByteValue) - case _: SmallIntType => - val tI16Value = new TI16Value - if (row.getField(ordinal) != null) { - tI16Value.setValue(row.getField(ordinal).asInstanceOf[Short]) - } - TColumnValue.i16Val(tI16Value) - case _: IntType => - val tI32Value = new TI32Value - if (row.getField(ordinal) != null) { - tI32Value.setValue(row.getField(ordinal).asInstanceOf[Int]) - } - TColumnValue.i32Val(tI32Value) - case _: BigIntType => - val tI64Value = new TI64Value - if (row.getField(ordinal) != null) { - tI64Value.setValue(row.getField(ordinal).asInstanceOf[Long]) - } - TColumnValue.i64Val(tI64Value) - case _: FloatType => - val tDoubleValue = new TDoubleValue - if (row.getField(ordinal) != null) { - val doubleValue = lang.Double.valueOf(row.getField(ordinal).asInstanceOf[Float].toString) - tDoubleValue.setValue(doubleValue) - } - TColumnValue.doubleVal(tDoubleValue) - case _: DoubleType => - val tDoubleValue = new TDoubleValue - if (row.getField(ordinal) != null) { - tDoubleValue.setValue(row.getField(ordinal).asInstanceOf[Double]) - } - TColumnValue.doubleVal(tDoubleValue) - case t @ (_: VarCharType | _: CharType) => - val tStringValue = new TStringValue - val fieldValue = row.getField(ordinal) - fieldValue match { - case value: String => - tStringValue.setValue(value) - case value: StringData => - tStringValue.setValue(value.toString) - case null => - tStringValue.setValue(null) - case other => - throw new IllegalArgumentException( - s"Unsupported conversion class ${other.getClass} " + - s"for type ${t.getClass}.") - } - TColumnValue.stringVal(tStringValue) - case _: LocalZonedTimestampType => - val tStringValue = new TStringValue - val fieldValue = row.getField(ordinal) - tStringValue.setValue(TIMESTAMP_LZT_FORMATTER.format( - ZonedDateTime.ofInstant(fieldValue.asInstanceOf[Instant], zoneId))) - TColumnValue.stringVal(tStringValue) - case t => - val tStringValue = new TStringValue - if (row.getField(ordinal) != null) { - tStringValue.setValue(toHiveString((row.getField(ordinal), t))) - } - TColumnValue.stringVal(tStringValue) - } - } - - implicit private def bitSetToBuffer(bitSet: java.util.BitSet): ByteBuffer = { - ByteBuffer.wrap(bitSet.toByteArray) - } - - private def toTColumn( - rows: Seq[Row], - ordinal: Int, - logicalType: LogicalType, - zoneId: ZoneId): TColumn = { - val nulls = new java.util.BitSet() - // for each column, determine the conversion class by sampling the first non-value value - // if there's no row, set the entire column empty - val sampleField = rows.iterator.map(_.getField(ordinal)).find(_ ne null).orNull - logicalType match { - case _: BooleanType => - val values = getOrSetAsNull[lang.Boolean](rows, ordinal, nulls, true) - TColumn.boolVal(new TBoolColumn(values, nulls)) - case _: TinyIntType => - val values = getOrSetAsNull[lang.Byte](rows, ordinal, nulls, 0.toByte) - TColumn.byteVal(new TByteColumn(values, nulls)) - case _: SmallIntType => - val values = getOrSetAsNull[lang.Short](rows, ordinal, nulls, 0.toShort) - TColumn.i16Val(new TI16Column(values, nulls)) - case _: IntType => - val values = getOrSetAsNull[lang.Integer](rows, ordinal, nulls, 0) - TColumn.i32Val(new TI32Column(values, nulls)) - case _: BigIntType => - val values = getOrSetAsNull[lang.Long](rows, ordinal, nulls, 0L) - TColumn.i64Val(new TI64Column(values, nulls)) - case _: FloatType => - val values = getOrSetAsNull[lang.Float](rows, ordinal, nulls, 0.0f) - .asScala.map(n => lang.Double.valueOf(n.toString)).asJava - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - case _: DoubleType => - val values = getOrSetAsNull[lang.Double](rows, ordinal, nulls, 0.0) - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - case t @ (_: VarCharType | _: CharType) => - val values: util.List[String] = new util.ArrayList[String](0) - sampleField match { - case _: String => - values.addAll(getOrSetAsNull[String](rows, ordinal, nulls, "")) - case _: StringData => - val stringDataValues = - getOrSetAsNull[StringData](rows, ordinal, nulls, StringData.fromString("")) - stringDataValues.forEach(e => values.add(e.toString)) - case null => - values.addAll(getOrSetAsNull[String](rows, ordinal, nulls, "")) - case other => - throw new IllegalArgumentException( - s"Unsupported conversion class ${other.getClass} " + - s"for type ${t.getClass}.") - } - TColumn.stringVal(new TStringColumn(values, nulls)) - case _: LocalZonedTimestampType => - val values = getOrSetAsNull[Instant](rows, ordinal, nulls, Instant.EPOCH) - .toArray().map(v => - TIMESTAMP_LZT_FORMATTER.format( - ZonedDateTime.ofInstant(v.asInstanceOf[Instant], zoneId))) - TColumn.stringVal(new TStringColumn(values.toList.asJava, nulls)) - case _ => - var i = 0 - val rowSize = rows.length - val values = new java.util.ArrayList[String](rowSize) - while (i < rowSize) { - val row = rows(i) - nulls.set(i, row.getField(ordinal) == null) - val value = - if (row.getField(ordinal) == null) { - "" - } else { - toHiveString((row.getField(ordinal), logicalType)) - } - values.add(value) - i += 1 - } - TColumn.stringVal(new TStringColumn(values, nulls)) - } - } - - private def getOrSetAsNull[T]( - rows: Seq[Row], - ordinal: Int, - nulls: java.util.BitSet, - defaultVal: T): java.util.List[T] = { - val size = rows.length - val ret = new java.util.ArrayList[T](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row.getField(ordinal) == null - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row.getFieldAs[T](ordinal)) - } - idx += 1 - } - ret - } - def toTColumnDesc(field: Column, pos: Int): TColumnDesc = { val tColumnDesc = new TColumnDesc() tColumnDesc.setColumnName(field.getName) diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSQLSessionManager.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSQLSessionManager.scala index b7cd462172f..8627e5a2475 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSQLSessionManager.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSQLSessionManager.scala @@ -23,12 +23,12 @@ import scala.collection.JavaConverters.mapAsJavaMap import org.apache.flink.table.gateway.api.session.SessionEnvironment import org.apache.flink.table.gateway.rest.util.SqlGatewayRestAPIVersion import org.apache.flink.table.gateway.service.context.DefaultContext -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.flink.operation.FlinkSQLOperationManager import org.apache.kyuubi.engine.flink.shim.FlinkSessionManager import org.apache.kyuubi.session.{Session, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class FlinkSQLSessionManager(engineContext: DefaultContext) extends SessionManager("FlinkSQLSessionManager") { diff --git a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSessionImpl.scala b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSessionImpl.scala index b8d1f85692b..624c3ad9465 100644 --- a/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSessionImpl.scala +++ b/externals/kyuubi-flink-sql-engine/src/main/scala/org/apache/kyuubi/engine/flink/session/FlinkSessionImpl.scala @@ -25,13 +25,14 @@ import org.apache.flink.table.client.gateway.SqlExecutionException import org.apache.flink.table.gateway.api.operation.OperationHandle import org.apache.flink.table.gateway.service.context.SessionContext import org.apache.flink.table.gateway.service.session.{Session => FSession} -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} import org.apache.kyuubi.KyuubiSQLException +import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.flink.FlinkEngineUtils import org.apache.kyuubi.engine.flink.udf.KDFRegistry import org.apache.kyuubi.session.{AbstractSession, SessionHandle, SessionManager, USE_CATALOG, USE_DATABASE} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} class FlinkSessionImpl( protocol: TProtocolVersion, @@ -64,6 +65,15 @@ class FlinkSessionImpl( override def open(): Unit = { val executor = fSession.createExecutor(Configuration.fromMap(fSession.getSessionConfig)) + sessionManager.getConf.get(ENGINE_SESSION_FLINK_INITIALIZE_SQL).foreach { sql => + try { + executor.executeStatement(OperationHandle.create, sql) + } catch { + case NonFatal(e) => + throw KyuubiSQLException(s"execute ${ENGINE_SESSION_FLINK_INITIALIZE_SQL.key} $sql ", e) + } + } + val (useCatalogAndDatabaseConf, otherConf) = normalizedConf.partition { case (k, _) => Array(USE_CATALOG, USE_DATABASE).contains(k) } @@ -99,6 +109,7 @@ class FlinkSessionImpl( case TGetInfoType.CLI_SERVER_NAME | TGetInfoType.CLI_DBMS_NAME => TGetInfoValue.stringValue("Apache Flink") case TGetInfoType.CLI_DBMS_VER => TGetInfoValue.stringValue(EnvironmentInformation.getVersion) + case TGetInfoType.CLI_ODBC_KEYWORDS => TGetInfoValue.stringValue("Unimplemented") case _ => throw KyuubiSQLException(s"Unrecognized GetInfoType value: $infoType") } } diff --git a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineLocal.scala b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineLocal.scala index 92c1bcd83fc..1c4adce189d 100644 --- a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineLocal.scala +++ b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineLocal.scala @@ -23,7 +23,7 @@ import java.net.URI import java.nio.file.{Files, Paths} import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import org.apache.flink.configuration.{Configuration, RestOptions} import org.apache.flink.runtime.minicluster.{MiniCluster, MiniClusterConfiguration} @@ -32,6 +32,7 @@ import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiException, KyuubiFunSuite, SCALA import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ADDRESSES +import org.apache.kyuubi.util.command.CommandLineUtils._ import org.apache.kyuubi.zookeeper.EmbeddedZookeeper import org.apache.kyuubi.zookeeper.ZookeeperConf.{ZK_CLIENT_PORT, ZK_CLIENT_PORT_ADDRESS} @@ -45,7 +46,7 @@ trait WithFlinkSQLEngineLocal extends KyuubiFunSuite with WithFlinkTestResources private var zkServer: EmbeddedZookeeper = _ - protected val conf: KyuubiConf = FlinkSQLEngine.kyuubiConf + protected val conf: KyuubiConf = new KyuubiConf(false) protected def engineRefId: String @@ -60,7 +61,6 @@ trait WithFlinkSQLEngineLocal extends KyuubiFunSuite with WithFlinkTestResources } } withKyuubiConf.foreach { case (k, v) => - System.setProperty(k, v) conf.set(k, v) } @@ -112,7 +112,7 @@ trait WithFlinkSQLEngineLocal extends KyuubiFunSuite with WithFlinkTestResources processBuilder.environment().putAll(envs.asJava) conf.set(ENGINE_FLINK_EXTRA_CLASSPATH, udfJar.getAbsolutePath) - val command = new ArrayBuffer[String]() + val command = new mutable.ListBuffer[String]() command += envs("JAVA_EXEC") @@ -123,8 +123,7 @@ trait WithFlinkSQLEngineLocal extends KyuubiFunSuite with WithFlinkTestResources command += javaOptions.get } - command += "-cp" - val classpathEntries = new java.util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] // flink engine runtime jar mainResource(envs).foreach(classpathEntries.add) // flink sql jars @@ -164,13 +163,11 @@ trait WithFlinkSQLEngineLocal extends KyuubiFunSuite with WithFlinkTestResources classpathEntries.add(s"$devHadoopJars${File.separator}*") } } - command += classpathEntries.asScala.mkString(File.pathSeparator) + command ++= genClasspathOption(classpathEntries) + command += "org.apache.kyuubi.engine.flink.FlinkSQLEngine" - conf.getAll.foreach { case (k, v) => - command += "--conf" - command += s"$k=$v" - } + command ++= confKeyValues(conf.getAll) processBuilder.command(command.toList.asJava) processBuilder.redirectOutput(Redirect.INHERIT) diff --git a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineOnYarn.scala b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineOnYarn.scala index 49fb947a3ec..730a2646bed 100644 --- a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineOnYarn.scala +++ b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/WithFlinkSQLEngineOnYarn.scala @@ -34,6 +34,7 @@ import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite, SCALA_COMPILE_VERSION, import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.{ENGINE_FLINK_APPLICATION_JARS, KYUUBI_HOME} import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ADDRESSES +import org.apache.kyuubi.util.command.CommandLineUtils._ import org.apache.kyuubi.zookeeper.EmbeddedZookeeper import org.apache.kyuubi.zookeeper.ZookeeperConf.{ZK_CLIENT_PORT, ZK_CLIENT_PORT_ADDRESS} @@ -179,10 +180,7 @@ trait WithFlinkSQLEngineOnYarn extends KyuubiFunSuite with WithFlinkTestResource conf.set(k, v) } - for ((k, v) <- conf.getAll) { - command += "--conf" - command += s"$k=$v" - } + command ++= confKeyValues(conf.getAll) processBuilder.command(command.toList.asJava) processBuilder.redirectOutput(Redirect.INHERIT) diff --git a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkEngineInitializeSuite.scala b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkEngineInitializeSuite.scala new file mode 100644 index 00000000000..c98d07cc48c --- /dev/null +++ b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkEngineInitializeSuite.scala @@ -0,0 +1,104 @@ +/* + * 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.kyuubi.engine.flink.operation + +import java.util.UUID + +import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY +import org.apache.kyuubi.engine.ShareLevel +import org.apache.kyuubi.engine.flink.{WithDiscoveryFlinkSQLEngine, WithFlinkSQLEngineLocal} +import org.apache.kyuubi.ha.HighAvailabilityConf.{HA_ENGINE_REF_ID, HA_NAMESPACE} +import org.apache.kyuubi.operation.{HiveJDBCTestHelper, NoneMode} + +class FlinkEngineInitializeSuite extends HiveJDBCTestHelper + with WithDiscoveryFlinkSQLEngine with WithFlinkSQLEngineLocal { + + protected def jdbcUrl: String = getFlinkEngineServiceUrl + + protected val ENGINE_INITIALIZE_SQL_VALUE: String = + "show databases;" + + protected val ENGINE_SESSION_INITIALIZE_SQL_VALUE: String = + """create catalog cat_b with ('type'='generic_in_memory'); + |create table blackhole(i int) with ('connector'='blackhole'); + |create table datagen(i int) with ( + |'connector'='datagen', + |'fields.i.kind'='sequence', + |'fields.i.start'='1', + |'fields.i.end'='10')""".stripMargin + + override def withKyuubiConf: Map[String, String] = { + Map( + "flink.execution.target" -> "remote", + "flink.high-availability.cluster-id" -> "flink-mini-cluster", + "flink.app.name" -> "kyuubi_connection_flink_kandy", + HA_NAMESPACE.key -> namespace, + HA_ENGINE_REF_ID.key -> engineRefId, + ENGINE_TYPE.key -> "FLINK_SQL", + ENGINE_SHARE_LEVEL.key -> shareLevel, + OPERATION_PLAN_ONLY_MODE.key -> NoneMode.name, + ENGINE_FLINK_INITIALIZE_SQL.key -> ENGINE_INITIALIZE_SQL_VALUE, + ENGINE_SESSION_FLINK_INITIALIZE_SQL.key -> ENGINE_SESSION_INITIALIZE_SQL_VALUE, + KYUUBI_SESSION_USER_KEY -> "kandy") + } + + override protected def engineRefId: String = UUID.randomUUID().toString + + def namespace: String = "/kyuubi/flink-local-engine-test" + + def shareLevel: String = ShareLevel.USER.toString + + def engineType: String = "flink" + + test("execute statement - kyuubi engine initialize") { + withJdbcStatement() { statement => + var resultSet = statement.executeQuery("show catalogs") + val expectedCatalogs = Set("default_catalog", "cat_b") + var actualCatalogs = Set[String]() + while (resultSet.next()) { + actualCatalogs += resultSet.getString(1) + } + assert(expectedCatalogs.subsetOf(actualCatalogs)) + + resultSet = statement.executeQuery("show databases") + assert(resultSet.next()) + assert(resultSet.getString(1) === "default_database") + assert(!resultSet.next()) + + val expectedTables = Set("blackhole", "datagen") + resultSet = statement.executeQuery("show tables") + while (resultSet.next()) { + assert(expectedTables.contains(resultSet.getString(1))) + } + assert(!resultSet.next()) + + var dropResult = statement.executeQuery("drop catalog cat_b") + assert(dropResult.next()) + assert(dropResult.getString(1) === "OK") + + dropResult = statement.executeQuery("drop table blackhole") + assert(dropResult.next()) + assert(dropResult.getString(1) === "OK") + + dropResult = statement.executeQuery("drop table datagen") + assert(dropResult.next()) + assert(dropResult.getString(1) === "OK") + } + } +} diff --git a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperationSuite.scala b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperationSuite.scala index 8e7c35a95a4..59d5fde3467 100644 --- a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperationSuite.scala +++ b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/operation/FlinkOperationSuite.scala @@ -26,18 +26,18 @@ import scala.collection.JavaConverters._ import org.apache.flink.api.common.JobID import org.apache.flink.configuration.PipelineOptions import org.apache.flink.table.types.logical.LogicalTypeRoot -import org.apache.hive.service.rpc.thrift._ import org.apache.kyuubi.Utils import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.engine.flink.FlinkEngineUtils.FLINK_RUNTIME_VERSION import org.apache.kyuubi.engine.flink.WithFlinkTestResources -import org.apache.kyuubi.engine.flink.result.Constants +import org.apache.kyuubi.engine.flink.result.{CommandStrings, Constants} import org.apache.kyuubi.engine.flink.util.TestUserClassLoaderJar import org.apache.kyuubi.jdbc.hive.{KyuubiSQLException, KyuubiStatement} import org.apache.kyuubi.jdbc.hive.common.TimestampTZ import org.apache.kyuubi.operation.HiveJDBCTestHelper import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ abstract class FlinkOperationSuite extends HiveJDBCTestHelper with WithFlinkTestResources { @@ -637,7 +637,9 @@ abstract class FlinkOperationSuite extends HiveJDBCTestHelper with WithFlinkTest test("execute statement - show/stop jobs") { if (FLINK_RUNTIME_VERSION >= "1.17") { - withSessionConf()(Map(ENGINE_FLINK_MAX_ROWS.key -> "10"))(Map.empty) { + // use a bigger value to ensure all tasks of the streaming query run until + // we explicitly stop the job. + withSessionConf()(Map(ENGINE_FLINK_MAX_ROWS.key -> "10000"))(Map.empty) { withMultipleConnectionJdbcStatement()({ statement => statement.executeQuery( "create table tbl_a (a int) with (" + @@ -1146,6 +1148,22 @@ abstract class FlinkOperationSuite extends HiveJDBCTestHelper with WithFlinkTest assert(rows === 200) } } + if (FLINK_RUNTIME_VERSION >= "1.17") { + withSessionConf()(Map(ENGINE_FLINK_MAX_ROWS.key -> "10"))(Map.empty) { + withJdbcStatement() { statement => + for (i <- 0 to 10) { + statement.execute(s"create table tbl_src$i (a bigint) " + + s"with ('connector' = 'blackhole')") + } + val resultSet = statement.executeQuery("show tables") + var rows = 0 + while (resultSet.next()) { + rows += 1 + } + assert(rows === 11) + } + } + } } test("execute statement - add/show jar") { @@ -1253,7 +1271,7 @@ abstract class FlinkOperationSuite extends HiveJDBCTestHelper with WithFlinkTest test("test result fetch timeout") { val exception = intercept[KyuubiSQLException]( - withSessionConf()(Map(ENGINE_FLINK_FETCH_TIMEOUT.key -> "60000"))() { + withSessionConf()(Map(ENGINE_FLINK_FETCH_TIMEOUT.key -> "PT60S"))() { withJdbcStatement("tbl_a") { stmt => stmt.executeQuery("create table tbl_a (a int) " + "with ('connector' = 'datagen', 'rows-per-second'='0')") @@ -1263,4 +1281,14 @@ abstract class FlinkOperationSuite extends HiveJDBCTestHelper with WithFlinkTest }) assert(exception.getMessage === "Futures timed out after [60000 milliseconds]") } + + test("execute statement - help") { + withJdbcStatement() { stmt => + val resultSet = stmt.executeQuery("help") + val metadata = resultSet.getMetaData + assert(metadata.getColumnName(1) === "result") + assert(resultSet.next()) + assert(resultSet.getString(1).equals(CommandStrings.MESSAGE_HELP.toString)) + } + } } diff --git a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/result/ResultSetSuite.scala b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/result/ResultSetSuite.scala index 9ee5c658bc9..5e58d433f91 100644 --- a/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/result/ResultSetSuite.scala +++ b/externals/kyuubi-flink-sql-engine/src/test/scala/org/apache/kyuubi/engine/flink/result/ResultSetSuite.scala @@ -25,7 +25,7 @@ import org.apache.flink.table.data.StringData import org.apache.flink.types.Row import org.apache.kyuubi.KyuubiFunSuite -import org.apache.kyuubi.engine.flink.schema.RowSet +import org.apache.kyuubi.engine.flink.schema.FlinkTRowSetGenerator class ResultSetSuite extends KyuubiFunSuite { @@ -47,9 +47,9 @@ class ResultSetSuite extends KyuubiFunSuite { .build val timeZone = ZoneId.of("America/Los_Angeles") - assert(RowSet.toRowBaseSet(rowsNew, resultSetNew, timeZone) - === RowSet.toRowBaseSet(rowsOld, resultSetOld, timeZone)) - assert(RowSet.toColumnBasedSet(rowsNew, resultSetNew, timeZone) - === RowSet.toColumnBasedSet(rowsOld, resultSetOld, timeZone)) + assert(new FlinkTRowSetGenerator(timeZone).toRowBasedSet(rowsNew, resultSetNew) + === new FlinkTRowSetGenerator(timeZone).toRowBasedSet(rowsOld, resultSetOld)) + assert(new FlinkTRowSetGenerator(timeZone).toColumnBasedSet(rowsNew, resultSetNew) + === new FlinkTRowSetGenerator(timeZone).toColumnBasedSet(rowsOld, resultSetOld)) } } diff --git a/externals/kyuubi-hive-sql-engine/pom.xml b/externals/kyuubi-hive-sql-engine/pom.xml index caed7e27c37..89f2395f043 100644 --- a/externals/kyuubi-hive-sql-engine/pom.xml +++ b/externals/kyuubi-hive-sql-engine/pom.xml @@ -50,18 +50,6 @@ ${project.version} - - org.apache.hive - hive-service-rpc - provided - - - - org.apache.thrift - libfb303 - provided - - com.google.code.findbugs jsr305 @@ -73,12 +61,6 @@ commons-collections - - org.apache.thrift - libthrift - provided - - com.google.guava failureaccess @@ -206,6 +188,18 @@ + + + com.fasterxml.jackson + ${kyuubi.shade.packageName}.com.fasterxml.jackson + + com.fasterxml.jackson.** + + + + + +
    diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveSQLEngine.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveSQLEngine.scala index 3cc426c435a..f22e281fbaa 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveSQLEngine.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveSQLEngine.scala @@ -79,6 +79,14 @@ object HiveSQLEngine extends Logging { kyuubiConf.setIfMissing(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0) kyuubiConf.setIfMissing(HA_ZK_CONN_RETRY_POLICY, RetryPolicies.N_TIME.toString) + // align with the operational behavior of HiveServer2, it is necessary to + // include the `hiveserver2-site.xml` configuration within the HiveConf settings. + // for instance, upon the installation of the Hive Ranger plugin, authorization + // configurations are appended to the `hiveserver2-site.xml` file. Similarly, to activate + // the Ranger plugin for the Hive engine within Kyuubi, it is essential for the Hive engine + // to load the `hiveserver2-site.xml` file. This ensures that the Hive engine's + // security features are consistent with those managed by HiveServer2. See [KYUUBI #5878]. + hiveConf.addResource("hiveserver2-site.xml") for ((k, v) <- kyuubiConf.getAll) { hiveConf.set(k, v) } @@ -130,7 +138,15 @@ object HiveSQLEngine extends Logging { } else { val effectiveUser = UserGroupInformation.createProxyUser(sessionUser.get, realUser) effectiveUser.doAs(new PrivilegedExceptionAction[Unit] { - override def run(): Unit = startEngine() + override def run(): Unit = { + val engineCredentials = + kyuubiConf.getOption(KyuubiReservedKeys.KYUUBI_ENGINE_CREDENTIALS_KEY) + kyuubiConf.unset(KyuubiReservedKeys.KYUUBI_ENGINE_CREDENTIALS_KEY) + engineCredentials.filter(_.nonEmpty).foreach { credentials => + HiveTBinaryFrontendService.renewDelegationToken(credentials) + } + startEngine() + } }) } diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveTBinaryFrontendService.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveTBinaryFrontendService.scala index d7cc801d3f6..082e4d12f69 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveTBinaryFrontendService.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/HiveTBinaryFrontendService.scala @@ -17,11 +17,19 @@ package org.apache.kyuubi.engine.hive +import org.apache.hadoop.io.Text +import org.apache.hadoop.security.UserGroupInformation + +import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.ha.client.{EngineServiceDiscovery, ServiceDiscovery} import org.apache.kyuubi.service.{Serverable, Service, TBinaryFrontendService} +import org.apache.kyuubi.service.TFrontendService.OK_STATUS +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TRenewDelegationTokenReq, TRenewDelegationTokenResp} +import org.apache.kyuubi.util.KyuubiHadoopUtils class HiveTBinaryFrontendService(override val serverable: Serverable) extends TBinaryFrontendService("HiveTBinaryFrontend") { + import HiveTBinaryFrontendService._ override lazy val discoveryService: Option[Service] = { if (ServiceDiscovery.supportServiceDiscovery(conf)) { @@ -30,4 +38,39 @@ class HiveTBinaryFrontendService(override val serverable: Serverable) None } } + + override def RenewDelegationToken(req: TRenewDelegationTokenReq): TRenewDelegationTokenResp = { + debug(req.toString) + + // We hacked `TCLIService.Iface.RenewDelegationToken` to transfer Credentials from Kyuubi + // Server to Hive SQL engine + val resp = new TRenewDelegationTokenResp() + try { + renewDelegationToken(req.getDelegationToken) + resp.setStatus(OK_STATUS) + } catch { + case e: Exception => + warn("Error renew delegation tokens: ", e) + resp.setStatus(KyuubiSQLException.toTStatus(e)) + } + resp + } +} + +object HiveTBinaryFrontendService { + + def renewDelegationToken(tokenStr: String): Unit = { + val currentUser = UserGroupInformation.getCurrentUser + // `currentUser` is either `UserGroupInformation.getLoginUser` or a proxy user. + // If `currentUser` is a proxy user, it needs a HIVE_DELEGATION_TOKEN to pass + // HiveMetastoreClient authentication. + if (currentUser.getAuthenticationMethod == UserGroupInformation.AuthenticationMethod.PROXY) { + val newCreds = KyuubiHadoopUtils.decodeCredentials(tokenStr) + KyuubiHadoopUtils.getTokenMap(newCreds).values + .find(_.getKind == new Text("HIVE_DELEGATION_TOKEN")) + .foreach { token => + UserGroupInformation.getCurrentUser.addToken(token) + } + } + } } diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperation.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperation.scala index 9759fa00be4..11cb5c5dfb5 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperation.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperation.scala @@ -19,16 +19,18 @@ package org.apache.kyuubi.engine.hive.operation import java.util.concurrent.Future +import org.apache.hive.service.cli.{FetchOrientation => HiveFetchOrientation} import org.apache.hive.service.cli.operation.{Operation, OperationManager} import org.apache.hive.service.cli.session.{HiveSession, SessionManager => HiveSessionManager} -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY import org.apache.kyuubi.engine.hive.session.HiveSessionImpl +import org.apache.kyuubi.engine.hive.util.HiveRpcUtils import org.apache.kyuubi.operation.{AbstractOperation, FetchOrientation, OperationState, OperationStatus} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} abstract class HiveOperation(session: Session) extends AbstractOperation(session) { @@ -90,7 +92,7 @@ abstract class HiveOperation(session: Session) extends AbstractOperation(session override def getResultSetMetadata: TGetResultSetMetadataResp = { val schema = internalHiveOperation.getResultSetSchema.toTTableSchema val resp = new TGetResultSetMetadataResp - resp.setSchema(schema) + resp.setSchema(HiveRpcUtils.asKyuubi(schema)) resp.setStatus(OK_STATUS) resp } @@ -98,18 +100,18 @@ abstract class HiveOperation(session: Session) extends AbstractOperation(session override def getNextRowSetInternal( order: FetchOrientation, rowSetSize: Int): TFetchResultsResp = { - val tOrder = FetchOrientation.toTFetchOrientation(order) - val hiveOrder = org.apache.hive.service.cli.FetchOrientation.getFetchOrientation(tOrder) + val hiveTOrder = HiveRpcUtils.asHive(FetchOrientation.toTFetchOrientation(order)) + val hiveOrder = HiveFetchOrientation.getFetchOrientation(hiveTOrder) val rowSet = internalHiveOperation.getNextRowSet(hiveOrder, rowSetSize) val resp = new TFetchResultsResp(OK_STATUS) - resp.setResults(rowSet.toTRowSet) + resp.setResults(HiveRpcUtils.asKyuubi(rowSet.toTRowSet)) resp.setHasMoreRows(false) resp } def getOperationLogRowSet(order: FetchOrientation, rowSetSize: Int): TFetchResultsResp = { - val tOrder = FetchOrientation.toTFetchOrientation(order) - val hiveOrder = org.apache.hive.service.cli.FetchOrientation.getFetchOrientation(tOrder) + val hiveTOrder = HiveRpcUtils.asHive(FetchOrientation.toTFetchOrientation(order)) + val hiveOrder = HiveFetchOrientation.getFetchOrientation(hiveTOrder) val handle = internalHiveOperation.getHandle val rowSet = delegatedOperationManager.getOperationLogRowSet( handle, @@ -117,7 +119,7 @@ abstract class HiveOperation(session: Session) extends AbstractOperation(session rowSetSize, hive.getHiveConf).toTRowSet val resp = new TFetchResultsResp(OK_STATUS) - resp.setResults(rowSet) + resp.setResults(HiveRpcUtils.asKyuubi(rowSet)) resp.setHasMoreRows(false) resp } diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationManager.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationManager.scala index 4e41e742e0b..faa7381ced7 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationManager.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationManager.scala @@ -20,13 +20,13 @@ package org.apache.kyuubi.engine.hive.operation import java.util.List import org.apache.hadoop.hive.conf.HiveConf.ConfVars -import org.apache.hive.service.rpc.thrift.TFetchResultsResp import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.engine.hive.session.HiveSessionImpl import org.apache.kyuubi.operation.{Operation, OperationHandle, OperationManager} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TFetchResultsResp class HiveOperationManager() extends OperationManager("HiveOperationManager") { // we use hive's operation log diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionImpl.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionImpl.scala index 5069b13798c..91db1cb8d0a 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionImpl.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionImpl.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.engine.hive.session -import java.util.HashMap +import java.util import scala.collection.JavaConverters._ import org.apache.hive.common.util.HiveVersionInfo import org.apache.hive.service.cli.session.HiveSession -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.engine.hive.events.HiveSessionEvent @@ -31,6 +30,8 @@ import org.apache.kyuubi.engine.hive.udf.KDFRegistry import org.apache.kyuubi.events.EventBus import org.apache.kyuubi.operation.{Operation, OperationHandle} import org.apache.kyuubi.session.{AbstractSession, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} +import org.apache.kyuubi.util.reflect.{DynFields, DynMethods} class HiveSessionImpl( protocol: TProtocolVersion, @@ -46,7 +47,7 @@ class HiveSessionImpl( private val sessionEvent = HiveSessionEvent(this) override def open(): Unit = { - val confClone = new HashMap[String, String]() + val confClone = new util.HashMap[String, String]() confClone.putAll(conf.asJava) // pass conf.asScala not support `put` method hive.open(confClone) KDFRegistry.registerAll() @@ -63,7 +64,22 @@ class HiveSessionImpl( case TGetInfoType.CLI_SERVER_NAME => TGetInfoValue.stringValue("Hive") case TGetInfoType.CLI_DBMS_NAME => TGetInfoValue.stringValue("Apache Hive") case TGetInfoType.CLI_DBMS_VER => TGetInfoValue.stringValue(HiveVersionInfo.getVersion) - case TGetInfoType.CLI_ODBC_KEYWORDS => TGetInfoValue.stringValue("Unimplemented") + case TGetInfoType.CLI_ODBC_KEYWORDS => + try { + // HIVE-17765 expose Hive keywords. + // exclude these keywords to be consistent with Hive behavior. + val excludes = DynFields.builder() + .hiddenImpl("org.apache.hive.service.cli.session.HiveSessionImpl", "ODBC_KEYWORDS") + .buildStaticChecked[util.Set[String]]().get() + val keywords = DynMethods.builder("getKeywords") + .impl("org.apache.hadoop.hive.ql.parse.ParseUtils", classOf[util.Set[String]]) + .buildStaticChecked() + .invoke[String](excludes) + TGetInfoValue.stringValue(keywords) + } catch { + case _: ReflectiveOperationException => + TGetInfoValue.stringValue("Unimplemented") + } case TGetInfoType.CLI_MAX_COLUMN_NAME_LEN | TGetInfoType.CLI_MAX_SCHEMA_NAME_LEN | TGetInfoType.CLI_MAX_TABLE_NAME_LEN => TGetInfoValue.lenValue(128) diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionManager.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionManager.scala index d09912770cc..ef98f5b0a11 100644 --- a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionManager.scala +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/session/HiveSessionManager.scala @@ -18,22 +18,31 @@ package org.apache.kyuubi.engine.hive.session import java.io.File +import java.util.{List => JList} import java.util.concurrent.Future import scala.collection.JavaConverters._ +import scala.language.reflectiveCalls import org.apache.hadoop.hive.conf.HiveConf +import org.apache.hadoop.hive.conf.HiveConf.ConfVars import org.apache.hive.service.cli.{SessionHandle => ImportedSessionHandle} -import org.apache.hive.service.cli.session.{HiveSessionImplwithUGI => ImportedHiveSessionImpl, HiveSessionProxy, SessionManager => ImportedHiveSessionManager} -import org.apache.hive.service.rpc.thrift.TProtocolVersion +import org.apache.hive.service.cli.session.{HiveSessionImpl => ImportedHiveSessionImpl} +import org.apache.hive.service.cli.session.{HiveSessionImplwithUGI => ImportedHiveSessionImplwithUGI} +import org.apache.hive.service.cli.session.{SessionManager => ImportedHiveSessionManager} +import org.apache.hive.service.cli.session.HiveSessionProxy +import org.apache.hive.service.rpc.thrift.{TProtocolVersion => HiveTProtocolVersion} import org.apache.kyuubi.config.KyuubiConf.ENGINE_SHARE_LEVEL import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.ShareLevel import org.apache.kyuubi.engine.hive.HiveSQLEngine import org.apache.kyuubi.engine.hive.operation.HiveOperationManager +import org.apache.kyuubi.engine.hive.util.HiveRpcUtils import org.apache.kyuubi.operation.OperationManager import org.apache.kyuubi.session.{Session, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion +import org.apache.kyuubi.util.reflect.DynConstructors class HiveSessionManager(engine: HiveSQLEngine) extends SessionManager("HiveSessionManager") { override protected def isServer: Boolean = false @@ -42,11 +51,14 @@ class HiveSessionManager(engine: HiveSQLEngine) extends SessionManager("HiveSess private val internalSessionManager = new ImportedHiveSessionManager(null) { + var doAsEnabled: Boolean = _ + /** * Avoid unnecessary hive initialization */ override def init(hiveConf: HiveConf): Unit = { // this.hiveConf = hiveConf + this.doAsEnabled = hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_ENABLE_DOAS) } /** @@ -75,21 +87,72 @@ class HiveSessionManager(engine: HiveSQLEngine) extends SessionManager("HiveSess conf: Map[String, String]): Session = { conf.get(KYUUBI_SESSION_HANDLE_KEY).map(SessionHandle.fromUUID).flatMap( getSessionOption).getOrElse { + val hiveProtocol = HiveRpcUtils.asHive(protocol) val sessionHandle = conf.get(KYUUBI_SESSION_HANDLE_KEY).map(SessionHandle.fromUUID).getOrElse(SessionHandle()) - val hive = { - val sessionWithUGI = new ImportedHiveSessionImpl( - new ImportedSessionHandle(sessionHandle.toTSessionHandle, protocol), - protocol, - user, - password, - HiveSQLEngine.hiveConf, - ipAddress, - null, - Seq(ipAddress).asJava) + val hiveTSessionHandle = HiveRpcUtils.asHive(sessionHandle.toTSessionHandle) + val hive = if (internalSessionManager.doAsEnabled) { + val sessionWithUGI = DynConstructors.builder() + .impl( // for Hive 3.1 + classOf[ImportedHiveSessionImplwithUGI], + classOf[ImportedSessionHandle], + classOf[HiveTProtocolVersion], + classOf[String], + classOf[String], + classOf[HiveConf], + classOf[String], + classOf[String], + classOf[JList[String]]) + .impl( // for Hive 2.3 + classOf[ImportedHiveSessionImplwithUGI], + classOf[ImportedSessionHandle], + classOf[HiveTProtocolVersion], + classOf[String], + classOf[String], + classOf[HiveConf], + classOf[String], + classOf[String]) + .build[ImportedHiveSessionImplwithUGI]() + .newInstance( + new ImportedSessionHandle(hiveTSessionHandle, hiveProtocol), + hiveProtocol, + user, + password, + HiveSQLEngine.hiveConf, + ipAddress, + null, + Seq(ipAddress).asJava) val proxy = HiveSessionProxy.getProxy(sessionWithUGI, sessionWithUGI.getSessionUgi) sessionWithUGI.setProxySession(proxy) proxy + } else { + DynConstructors.builder() + .impl( // for Hive 3.1 + classOf[ImportedHiveSessionImpl], + classOf[ImportedSessionHandle], + classOf[HiveTProtocolVersion], + classOf[String], + classOf[String], + classOf[HiveConf], + classOf[String], + classOf[JList[String]]) + .impl( // for Hive 2.3 + classOf[ImportedHiveSessionImpl], + classOf[ImportedSessionHandle], + classOf[HiveTProtocolVersion], + classOf[String], + classOf[String], + classOf[HiveConf], + classOf[String]) + .build[ImportedHiveSessionImpl]() + .newInstance( + new ImportedSessionHandle(hiveTSessionHandle, hiveProtocol), + hiveProtocol, + user, + password, + HiveSQLEngine.hiveConf, + ipAddress, + Seq(ipAddress).asJava) } hive.setSessionManager(internalSessionManager) hive.setOperationManager(internalSessionManager.getOperationManager) @@ -104,7 +167,6 @@ class HiveSessionManager(engine: HiveSQLEngine) extends SessionManager("HiveSess sessionHandle, hive) } - } override def closeSession(sessionHandle: SessionHandle): Unit = { diff --git a/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/util/HiveRpcUtils.scala b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/util/HiveRpcUtils.scala new file mode 100644 index 00000000000..2dab173420d --- /dev/null +++ b/externals/kyuubi-hive-sql-engine/src/main/scala/org/apache/kyuubi/engine/hive/util/HiveRpcUtils.scala @@ -0,0 +1,72 @@ +/* + * 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.kyuubi.engine.hive.util + +import org.apache.hive.service.rpc.thrift.{TFetchOrientation => HiveTFetchOrientation, THandleIdentifier => HiveTHandleIdentifier, TProtocolVersion => HiveTProtocolVersion, TRowSet => HiveTRowSet, TSessionHandle => HiveTSessionHandle, TTableSchema => HiveTTableSchema} +import org.apache.thrift.protocol.{TCompactProtocol => HiveTCompactProtocol} +import org.apache.thrift.transport.{TMemoryBuffer => HiveTMemoryBuffer} + +import org.apache.kyuubi.Logging +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.protocol.TCompactProtocol +import org.apache.kyuubi.shaded.thrift.transport.TMemoryInputTransport + +object HiveRpcUtils extends Logging { + + def asHive(tProtocolVersion: TProtocolVersion): HiveTProtocolVersion = + Option(HiveTProtocolVersion.findByValue(tProtocolVersion.getValue)).getOrElse { + val latestHiveTProtocolVersion = HiveTProtocolVersion.values().last + warn(s"Unsupported TProtocolVersion (Kyuubi): $tProtocolVersion, " + + s"fallback to latest TProtocolVersion (Hive): $latestHiveTProtocolVersion") + latestHiveTProtocolVersion + } + + def asHive(tHandleIdentifier: THandleIdentifier): HiveTHandleIdentifier = + new HiveTHandleIdentifier( + tHandleIdentifier.bufferForGuid(), + tHandleIdentifier.bufferForSecret()) + + def asHive(tSessionHandle: TSessionHandle): HiveTSessionHandle = + new HiveTSessionHandle(asHive(tSessionHandle.getSessionId)) + + def asHive(tFetchOrientation: TFetchOrientation): HiveTFetchOrientation = + Option(HiveTFetchOrientation.findByValue(tFetchOrientation.getValue)).getOrElse { + throw new UnsupportedOperationException( + s"Unsupported TFetchOrientation (Kyuubi): $tFetchOrientation") + } + + def asKyuubi(hiveTTableSchema: HiveTTableSchema): TTableSchema = { + val hiveBuffer = new HiveTMemoryBuffer(128) + hiveTTableSchema.write(new HiveTCompactProtocol(hiveBuffer)) + val bytes = hiveBuffer.getArray + val kyuubiBuffer = new TMemoryInputTransport(bytes) + val kyuubiTTableSchema = new TTableSchema + kyuubiTTableSchema.read(new TCompactProtocol(kyuubiBuffer)) + kyuubiTTableSchema + } + + def asKyuubi(hiveTRowSet: HiveTRowSet): TRowSet = { + val hiveBuffer = new HiveTMemoryBuffer(128) + hiveTRowSet.write(new HiveTCompactProtocol(hiveBuffer)) + val bytes = hiveBuffer.getArray + val kyuubiBuffer = new TMemoryInputTransport(bytes) + val kyuubiTRowSet = new TRowSet + kyuubiTRowSet.read(new TCompactProtocol(kyuubiBuffer)) + kyuubiTRowSet + } +} diff --git a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveCatalogDatabaseOperationSuite.scala b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveCatalogDatabaseOperationSuite.scala index a63de20c7de..7db2d7fdca3 100644 --- a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveCatalogDatabaseOperationSuite.scala +++ b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveCatalogDatabaseOperationSuite.scala @@ -23,6 +23,7 @@ import org.apache.kyuubi.Utils import org.apache.kyuubi.config.KyuubiConf.ENGINE_OPERATION_CONVERT_CATALOG_DATABASE_ENABLED import org.apache.kyuubi.engine.hive.HiveSQLEngine import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.util.command.CommandLineUtils._ class HiveCatalogDatabaseOperationSuite extends HiveJDBCTestHelper { @@ -30,9 +31,9 @@ class HiveCatalogDatabaseOperationSuite extends HiveJDBCTestHelper { val metastore = Utils.createTempDir(prefix = getClass.getSimpleName) metastore.toFile.delete() val args = Array( - "--conf", + CONF, s"javax.jdo.option.ConnectionURL=jdbc:derby:;databaseName=$metastore;create=true", - "--conf", + CONF, s"${ENGINE_OPERATION_CONVERT_CATALOG_DATABASE_ENABLED.key}=true") HiveSQLEngine.main(args) super.beforeAll() diff --git a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationSuite.scala b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationSuite.scala index eb10e0b4144..53cc9457ae1 100644 --- a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationSuite.scala +++ b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/operation/HiveOperationSuite.scala @@ -22,6 +22,7 @@ import org.apache.commons.lang3.{JavaVersion, SystemUtils} import org.apache.kyuubi.{HiveEngineTests, KYUUBI_VERSION, Utils} import org.apache.kyuubi.engine.hive.HiveSQLEngine import org.apache.kyuubi.jdbc.hive.KyuubiStatement +import org.apache.kyuubi.util.command.CommandLineUtils._ class HiveOperationSuite extends HiveEngineTests { @@ -29,7 +30,7 @@ class HiveOperationSuite extends HiveEngineTests { val metastore = Utils.createTempDir(prefix = getClass.getSimpleName) metastore.toFile.delete() val args = Array( - "--conf", + CONF, s"javax.jdo.option.ConnectionURL=jdbc:derby:;databaseName=$metastore;create=true") HiveSQLEngine.main(args) super.beforeAll() diff --git a/externals/kyuubi-jdbc-engine/pom.xml b/externals/kyuubi-jdbc-engine/pom.xml index 3c21fed570f..33f84da15c4 100644 --- a/externals/kyuubi-jdbc-engine/pom.xml +++ b/externals/kyuubi-jdbc-engine/pom.xml @@ -58,6 +58,18 @@ test + + com.dimafeng + testcontainers-scala-mysql_${scala.binary.version} + test + + + + com.dimafeng + testcontainers-scala-postgresql_${scala.binary.version} + test + + org.apache.kyuubi ${hive.jdbc.artifact} @@ -76,6 +88,12 @@ phoenix-queryserver-client test + + + org.postgresql + postgresql + test + diff --git a/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider b/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider index ec68c6884a9..0d8a2c58e5c 100644 --- a/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider +++ b/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider @@ -6,7 +6,7 @@ # (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 +# 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, @@ -16,4 +16,7 @@ # org.apache.kyuubi.engine.jdbc.doris.DorisConnectionProvider -org.apache.kyuubi.engine.jdbc.phoenix.PhoenixConnectionProvider \ No newline at end of file +org.apache.kyuubi.engine.jdbc.mysql.MySQLConnectionProvider +org.apache.kyuubi.engine.jdbc.phoenix.PhoenixConnectionProvider +org.apache.kyuubi.engine.jdbc.postgresql.PostgreSQLConnectionProvider +org.apache.kyuubi.engine.jdbc.starrocks.StarRocksConnectionProvider diff --git a/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.dialect.JdbcDialect b/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.dialect.JdbcDialect index cf84af61253..c5a75ec9c9f 100644 --- a/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.dialect.JdbcDialect +++ b/externals/kyuubi-jdbc-engine/src/main/resources/META-INF/services/org.apache.kyuubi.engine.jdbc.dialect.JdbcDialect @@ -6,7 +6,7 @@ # (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 +# 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, @@ -16,4 +16,7 @@ # org.apache.kyuubi.engine.jdbc.dialect.DorisDialect -org.apache.kyuubi.engine.jdbc.dialect.PhoenixDialect \ No newline at end of file +org.apache.kyuubi.engine.jdbc.dialect.MySQLDialect +org.apache.kyuubi.engine.jdbc.dialect.PhoenixDialect +org.apache.kyuubi.engine.jdbc.dialect.PostgreSQLDialect +org.apache.kyuubi.engine.jdbc.dialect.StarRocksDialect diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala index cb6e4b6c551..f8ec72dca93 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala @@ -27,7 +27,7 @@ import org.apache.kyuubi.util.reflect.ReflectUtils._ abstract class AbstractConnectionProvider extends Logging { protected val providers = loadProviders() - def getProviderClass(kyuubiConf: KyuubiConf): String = { + def getDriverClass(kyuubiConf: KyuubiConf): String = { val driverClass: Class[_ <: Driver] = Option( DynClasses.builder().impl(kyuubiConf.get(ENGINE_JDBC_DRIVER_CLASS).get) .orNull().build[Driver]()).getOrElse { @@ -38,7 +38,7 @@ abstract class AbstractConnectionProvider extends Logging { } def create(kyuubiConf: KyuubiConf): Connection = { - val filteredProviders = providers.filter(_.canHandle(getProviderClass(kyuubiConf))) + val filteredProviders = providers.filter(_.canHandle(getDriverClass(kyuubiConf))) if (filteredProviders.isEmpty) { throw new IllegalArgumentException( "Empty list of JDBC connection providers for the specified driver and options") @@ -57,10 +57,9 @@ abstract class AbstractConnectionProvider extends Logging { case None => // TODO if (filteredProviders.size != 1) { - throw new IllegalArgumentException( - "JDBC connection initiated but more than one connection provider was found. Use " + - s"${ENGINE_JDBC_CONNECTION_PROVIDER.key} option to select a specific provider. " + - s"Found active providers ${filteredProviders.mkString("[", ", ", "]")}") + warn("JDBC connection initiated but more than one connection provider was found. Use " + + s"${ENGINE_JDBC_CONNECTION_PROVIDER.key} option to select a specific provider. " + + s"Found active providers ${filteredProviders.mkString("[", ", ", "]")}") } filteredProviders.head } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/DorisDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/DorisDialect.scala index f7c1ace6473..e48a12a8991 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/DorisDialect.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/DorisDialect.scala @@ -15,120 +15,15 @@ * limitations under the License. */ package org.apache.kyuubi.engine.jdbc.dialect -import java.sql.{Connection, Statement} -import java.util -import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import org.apache.kyuubi.engine.jdbc.doris.{DorisSchemaHelper, DorisTRowSetGenerator} +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} -import org.apache.commons.lang3.StringUtils +class DorisDialect extends MySQLDialect { -import org.apache.kyuubi.engine.jdbc.doris.{DorisRowSetHelper, DorisSchemaHelper} -import org.apache.kyuubi.engine.jdbc.schema.{RowSetHelper, SchemaHelper} -import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ -import org.apache.kyuubi.session.Session + override def name(): String = "doris" -class DorisDialect extends JdbcDialect { + override def getTRowSetGenerator(): JdbcTRowSetGenerator = new DorisTRowSetGenerator - override def createStatement(connection: Connection, fetchSize: Int): Statement = { - val statement = super.createStatement(connection, fetchSize) - statement.setFetchSize(Integer.MIN_VALUE) - statement - } - - override def getTablesQuery( - catalog: String, - schema: String, - tableName: String, - tableTypes: util.List[String]): String = { - val tTypes = - if (tableTypes == null || tableTypes.isEmpty) { - Set("BASE TABLE", "SYSTEM VIEW", "VIEW") - } else { - tableTypes.asScala.toSet - } - val query = new StringBuilder( - s""" - |SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ENGINE, - |TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH, - |CREATE_TIME, UPDATE_TIME, TABLE_COLLATION, TABLE_COMMENT - |FROM INFORMATION_SCHEMA.TABLES - |""".stripMargin) - - val filters = ArrayBuffer[String]() - if (StringUtils.isNotBlank(catalog)) { - filters += s"$TABLE_CATALOG = '$catalog'" - } - - if (StringUtils.isNotBlank(schema)) { - filters += s"$TABLE_SCHEMA LIKE '$schema'" - } - - if (StringUtils.isNotBlank(tableName)) { - filters += s"$TABLE_NAME LIKE '$tableName'" - } - - if (tTypes.nonEmpty) { - filters += s"(${tTypes.map { tableType => s"$TABLE_TYPE = '$tableType'" } - .mkString(" OR ")})" - } - - if (filters.nonEmpty) { - query.append(" WHERE ") - query.append(filters.mkString(" AND ")) - } - - query.toString() - } - - override def getColumnsQuery( - session: Session, - catalogName: String, - schemaName: String, - tableName: String, - columnName: String): String = { - val query = new StringBuilder( - """ - |SELECT - |`TABLE_CATALOG`,`TABLE_SCHEMA`,`TABLE_NAME`, `COLUMN_NAME`,`ORDINAL_POSITION`, - |`COLUMN_DEFAULT`,`IS_NULLABLE`,`DATA_TYPE`,`CHARACTER_MAXIMUM_LENGTH`, - |`CHARACTER_OCTET_LENGTH`,`NUMERIC_PRECISION`,`NUMERIC_SCALE`,`DATETIME_PRECISION`, - |`CHARACTER_SET_NAME`,`COLLATION_NAME`,`COLUMN_TYPE`,`COLUMN_KEY`,`EXTRA`,`PRIVILEGES`, - |`COLUMN_COMMENT`,`COLUMN_SIZE`,`DECIMAL_DIGITS`,`GENERATION_EXPRESSION`,`SRS_ID` - |FROM information_schema.columns - |""".stripMargin) - - val filters = ArrayBuffer[String]() - if (StringUtils.isNotEmpty(catalogName)) { - filters += s"$TABLE_CATALOG = '$catalogName'" - } - if (StringUtils.isNotEmpty(schemaName)) { - filters += s"$TABLE_SCHEMA LIKE '$schemaName'" - } - if (StringUtils.isNotEmpty(tableName)) { - filters += s"$TABLE_NAME LIKE '$tableName'" - } - if (StringUtils.isNotEmpty(columnName)) { - filters += s"$COLUMN_NAME LIKE '$columnName'" - } - - if (filters.nonEmpty) { - query.append(" WHERE ") - query.append(filters.mkString(" AND ")) - } - - query.toString() - } - - override def getRowSetHelper(): RowSetHelper = { - new DorisRowSetHelper - } - - override def getSchemaHelper(): SchemaHelper = { - new DorisSchemaHelper - } - - override def name(): String = { - "doris" - } + override def getSchemaHelper(): SchemaHelper = new DorisSchemaHelper } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/JdbcDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/JdbcDialect.scala index 62e20a1d258..6c2d3b1e09d 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/JdbcDialect.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/JdbcDialect.scala @@ -22,7 +22,7 @@ import java.util import org.apache.kyuubi.{KyuubiException, KyuubiSQLException, Logging} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.{ENGINE_JDBC_CONNECTION_URL, ENGINE_JDBC_SHORT_NAME} -import org.apache.kyuubi.engine.jdbc.schema.{RowSetHelper, SchemaHelper} +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} import org.apache.kyuubi.engine.jdbc.util.SupportServiceLoader import org.apache.kyuubi.operation.Operation import org.apache.kyuubi.session.Session @@ -41,11 +41,11 @@ abstract class JdbcDialect extends SupportServiceLoader with Logging { throw KyuubiSQLException.featureNotSupported() } - def getCatalogsOperation(session: Session): Operation = { + def getCatalogsOperation(): String = { throw KyuubiSQLException.featureNotSupported() } - def getSchemasOperation(session: Session): Operation = { + def getSchemasOperation(catalog: String, schema: String): String = { throw KyuubiSQLException.featureNotSupported() } @@ -78,7 +78,7 @@ abstract class JdbcDialect extends SupportServiceLoader with Logging { throw KyuubiSQLException.featureNotSupported() } - def getRowSetHelper(): RowSetHelper + def getTRowSetGenerator(): JdbcTRowSetGenerator def getSchemaHelper(): SchemaHelper } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/MySQLDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/MySQLDialect.scala new file mode 100644 index 00000000000..e1392436391 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/MySQLDialect.scala @@ -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.kyuubi.engine.jdbc.dialect +import java.sql.{Connection, ResultSet, Statement} +import java.util + +import scala.collection.JavaConverters._ +import scala.collection.mutable.ArrayBuffer + +import org.apache.commons.lang3.StringUtils + +import org.apache.kyuubi.engine.jdbc.mysql.{MySQLSchemaHelper, MySQLTRowSetGenerator} +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} +import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.session.Session + +class MySQLDialect extends JdbcDialect { + override def createStatement(connection: Connection, fetchSize: Int): Statement = { + val statement = + connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + statement.setFetchSize(Integer.MIN_VALUE) + statement + } + + override def getTablesQuery( + catalog: String, + schema: String, + tableName: String, + tableTypes: util.List[String]): String = { + val tTypes = + if (tableTypes == null || tableTypes.isEmpty) { + Set("BASE TABLE", "SYSTEM VIEW", "VIEW") + } else { + tableTypes.asScala.toSet + } + val query = new StringBuilder( + s""" + |SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ENGINE, + |TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH, + |CREATE_TIME, UPDATE_TIME, TABLE_COLLATION, TABLE_COMMENT + |FROM INFORMATION_SCHEMA.TABLES + |""".stripMargin) + + val filters = ArrayBuffer[String]() + if (StringUtils.isNotBlank(catalog)) { + filters += s"$TABLE_CATALOG = '$catalog'" + } + + if (StringUtils.isNotBlank(schema)) { + filters += s"$TABLE_SCHEMA LIKE '$schema'" + } + + if (StringUtils.isNotBlank(tableName)) { + filters += s"$TABLE_NAME LIKE '$tableName'" + } + + if (tTypes.nonEmpty) { + filters += s"(${ + tTypes.map { tableType => s"$TABLE_TYPE = '$tableType'" } + .mkString(" OR ") + })" + } + + if (filters.nonEmpty) { + query.append(" WHERE ") + query.append(filters.mkString(" AND ")) + } + + query.toString() + } + + override def getColumnsQuery( + session: Session, + catalogName: String, + schemaName: String, + tableName: String, + columnName: String): String = { + val query = new StringBuilder( + """ + |SELECT + |`TABLE_CATALOG`,`TABLE_SCHEMA`,`TABLE_NAME`, `COLUMN_NAME`,`ORDINAL_POSITION`, + |`COLUMN_DEFAULT`,`IS_NULLABLE`,`DATA_TYPE`,`CHARACTER_MAXIMUM_LENGTH`, + |`CHARACTER_OCTET_LENGTH`,`NUMERIC_PRECISION`,`NUMERIC_SCALE`,`DATETIME_PRECISION`, + |`CHARACTER_SET_NAME`,`COLLATION_NAME`,`COLUMN_TYPE`,`COLUMN_KEY`,`EXTRA`,`PRIVILEGES`, + |`COLUMN_COMMENT`,`GENERATION_EXPRESSION` + |FROM information_schema.columns + |""".stripMargin) + + val filters = ArrayBuffer[String]() + if (StringUtils.isNotEmpty(catalogName)) { + filters += s"$TABLE_CATALOG = '$catalogName'" + } + if (StringUtils.isNotEmpty(schemaName)) { + filters += s"$TABLE_SCHEMA LIKE '$schemaName'" + } + if (StringUtils.isNotEmpty(tableName)) { + filters += s"$TABLE_NAME LIKE '$tableName'" + } + if (StringUtils.isNotEmpty(columnName)) { + filters += s"$COLUMN_NAME LIKE '$columnName'" + } + + if (filters.nonEmpty) { + query.append(" WHERE ") + query.append(filters.mkString(" AND ")) + } + + query.toString() + } + + override def getTRowSetGenerator(): JdbcTRowSetGenerator = new MySQLTRowSetGenerator + + override def getSchemaHelper(): SchemaHelper = { + new MySQLSchemaHelper + } + + override def name(): String = { + "mysql" + } +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PhoenixDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PhoenixDialect.scala index 4c8e8f26549..61440ac501e 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PhoenixDialect.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PhoenixDialect.scala @@ -22,8 +22,8 @@ import scala.collection.mutable.ArrayBuffer import org.apache.commons.lang3.StringUtils -import org.apache.kyuubi.engine.jdbc.phoenix.{PhoenixRowSetHelper, PhoenixSchemaHelper} -import org.apache.kyuubi.engine.jdbc.schema.{RowSetHelper, SchemaHelper} +import org.apache.kyuubi.engine.jdbc.phoenix.{PhoenixSchemaHelper, PhoenixTRowSetGenerator} +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ import org.apache.kyuubi.session.Session @@ -100,9 +100,7 @@ class PhoenixDialect extends JdbcDialect { query.toString() } - override def getRowSetHelper(): RowSetHelper = { - new PhoenixRowSetHelper - } + override def getTRowSetGenerator(): JdbcTRowSetGenerator = new PhoenixTRowSetGenerator override def getSchemaHelper(): SchemaHelper = { new PhoenixSchemaHelper diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PostgreSQLDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PostgreSQLDialect.scala new file mode 100644 index 00000000000..d3d4c8297b2 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/PostgreSQLDialect.scala @@ -0,0 +1,193 @@ +/* + * 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.kyuubi.engine.jdbc.dialect + +import java.sql.{Connection, ResultSet, Statement} +import java.util + +import scala.collection.JavaConverters._ +import scala.collection.mutable.ArrayBuffer + +import org.apache.commons.lang3.StringUtils + +import org.apache.kyuubi.KyuubiSQLException +import org.apache.kyuubi.engine.jdbc.postgresql.{PostgreSQLSchemaHelper, PostgreSQLTRowSetGenerator} +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} +import org.apache.kyuubi.operation.Operation +import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.session.Session + +class PostgreSQLDialect extends JdbcDialect { + + override def createStatement(connection: Connection, fetchSize: Int): Statement = { + val statement = + connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + if (connection.getAutoCommit) { + statement.setFetchSize(fetchSize) + } + statement + } + + override def getCatalogsOperation(): String = "SELECT CATALOG_NAME " + + "FROM INFORMATION_SCHEMA.INFORMATION_SCHEMA_CATALOG_NAME" + + override def getSchemasOperation( + catalog: String, + schema: String): String = { + val query = new StringBuilder( + s""" + |SELECT CATALOG_NAME, SCHEMA_NAME, SCHEMA_OWNER, + |DEFAULT_CHARACTER_SET_CATALOG, DEFAULT_CHARACTER_SET_SCHEMA, + |DEFAULT_CHARACTER_SET_NAME, SQL_PATH + |FROM INFORMATION_SCHEMA.SCHEMATA + |""".stripMargin) + + val filters = ArrayBuffer[String]() + if (StringUtils.isNotBlank(catalog)) { + filters += s"catalog_name LIKE '$catalog'" + } + + if (StringUtils.isNotBlank(schema)) { + filters += s"schema_name LIKE '$schema'" + } + + if (filters.nonEmpty) { + query.append(" WHERE ") + query.append(filters.mkString(" AND ")) + } + + query.toString() + } + + override def getTablesQuery( + catalog: String, + schema: String, + tableName: String, + tableTypes: util.List[String]): String = { + val tTypes = + if (tableTypes == null || tableTypes.isEmpty) { + Set("BASE TABLE", "VIEW") + } else { + tableTypes.asScala.toSet + } + val query = new StringBuilder( + s""" + |SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, + |SELF_REFERENCING_COLUMN_NAME, REFERENCE_GENERATION, USER_DEFINED_TYPE_CATALOG, + |USER_DEFINED_TYPE_SCHEMA,USER_DEFINED_TYPE_NAME, + |IS_INSERTABLE_INTO,IS_TYPED,COMMIT_ACTION + |FROM INFORMATION_SCHEMA.TABLES + |""".stripMargin) + + val filters = ArrayBuffer[String]() + if (StringUtils.isNotBlank(catalog)) { + filters += s"$TABLE_CATALOG LIKE '$catalog'" + } + + if (StringUtils.isNotBlank(schema)) { + filters += s"$TABLE_SCHEMA LIKE '$schema'" + } + + if (StringUtils.isNotBlank(tableName)) { + filters += s"$TABLE_NAME LIKE '$tableName'" + } + + if (tTypes.nonEmpty) { + filters += s"(${ + tTypes.map { tableType => s"$TABLE_TYPE = '$tableType'" } + .mkString(" OR ") + })" + } + + if (filters.nonEmpty) { + query.append(" WHERE ") + query.append(filters.mkString(" AND ")) + } + + query.toString() + } + + override def getTableTypesOperation(session: Session): Operation = { + throw KyuubiSQLException.featureNotSupported() + } + + override def getColumnsQuery( + session: Session, + catalogName: String, + schemaName: String, + tableName: String, + columnName: String): String = { + val query = new StringBuilder( + """ + |SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, + |COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, + |CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_PRECISION_RADIX, + |NUMERIC_SCALE, DATETIME_PRECISION, INTERVAL_TYPE, INTERVAL_PRECISION, + |CHARACTER_SET_CATALOG, CHARACTER_SET_SCHEMA, CHARACTER_SET_NAME, + |COLLATION_CATALOG, COLLATION_SCHEMA, COLLATION_NAME, DOMAIN_CATALOG, + |DOMAIN_SCHEMA, DOMAIN_NAME, UDT_CATALOG, UDT_SCHEMA, UDT_NAME, SCOPE_CATALOG, + |SCOPE_SCHEMA, SCOPE_NAME, MAXIMUM_CARDINALITY, DTD_IDENTIFIER, + |IS_SELF_REFERENCING, IS_IDENTITY, IDENTITY_GENERATION, IDENTITY_START, + |IDENTITY_INCREMENT, IDENTITY_MAXIMUM, IDENTITY_MINIMUM, IDENTITY_CYCLE, + |IS_GENERATED, GENERATION_EXPRESSION, IS_UPDATABLE + |FROM INFORMATION_SCHEMA.COLUMNS + |""".stripMargin) + + val filters = ArrayBuffer[String]() + if (StringUtils.isNotEmpty(catalogName)) { + filters += s"$TABLE_CATALOG LIKE '$catalogName'" + } + if (StringUtils.isNotEmpty(schemaName)) { + filters += s"$TABLE_SCHEMA LIKE '$schemaName'" + } + if (StringUtils.isNotEmpty(tableName)) { + filters += s"$TABLE_NAME LIKE '$tableName'" + } + if (StringUtils.isNotEmpty(columnName)) { + filters += s"$COLUMN_NAME LIKE '$columnName'" + } + + if (filters.nonEmpty) { + query.append(" WHERE ") + query.append(filters.mkString(" AND ")) + } + + query.toString() + } + + override def getFunctionsOperation(session: Session): Operation = { + throw KyuubiSQLException.featureNotSupported() + } + + override def getPrimaryKeysOperation(session: Session): Operation = { + throw KyuubiSQLException.featureNotSupported() + } + + override def getCrossReferenceOperation(session: Session): Operation = { + throw KyuubiSQLException.featureNotSupported() + } + + override def getTRowSetGenerator(): JdbcTRowSetGenerator = new PostgreSQLTRowSetGenerator + + override def getSchemaHelper(): SchemaHelper = { + new PostgreSQLSchemaHelper + } + + override def name(): String = { + "postgresql" + } +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/StarRocksDialect.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/StarRocksDialect.scala new file mode 100644 index 00000000000..aa4054eaa7c --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/dialect/StarRocksDialect.scala @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.engine.jdbc.dialect + +import org.apache.kyuubi.engine.jdbc.schema.{JdbcTRowSetGenerator, SchemaHelper} +import org.apache.kyuubi.engine.jdbc.starrocks.{StarRocksSchemaHelper, StarRocksTRowSetGenerator} + +class StarRocksDialect extends MySQLDialect { + override def name(): String = "starrocks" + + override def getTRowSetGenerator(): JdbcTRowSetGenerator = new StarRocksTRowSetGenerator + + override def getSchemaHelper(): SchemaHelper = new StarRocksSchemaHelper + +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisConnectionProvider.scala index 291e85d2d67..c38bf784561 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisConnectionProvider.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisConnectionProvider.scala @@ -16,9 +16,9 @@ */ package org.apache.kyuubi.engine.jdbc.doris -import org.apache.kyuubi.engine.jdbc.mysql.Mysql8ConnectionProvider +import org.apache.kyuubi.engine.jdbc.mysql.MySQL8ConnectionProvider -class DorisConnectionProvider extends Mysql8ConnectionProvider { +class DorisConnectionProvider extends MySQL8ConnectionProvider { override val name: String = classOf[DorisConnectionProvider].getSimpleName } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisSchemaHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisSchemaHelper.scala index b323d373142..a37ba4a39ac 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisSchemaHelper.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisSchemaHelper.scala @@ -16,13 +16,6 @@ */ package org.apache.kyuubi.engine.jdbc.doris -import org.apache.hive.service.rpc.thrift._ +import org.apache.kyuubi.engine.jdbc.mysql.MySQLSchemaHelper -import org.apache.kyuubi.engine.jdbc.schema.SchemaHelper - -class DorisSchemaHelper extends SchemaHelper { - - override def tinyIntToTTypeId: TTypeId = TTypeId.INT_TYPE - - override def smallIntToTTypeId: TTypeId = TTypeId.INT_TYPE -} +class DorisSchemaHelper extends MySQLSchemaHelper {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisTRowSetGenerator.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisTRowSetGenerator.scala new file mode 100644 index 00000000000..b77a7b31096 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/doris/DorisTRowSetGenerator.scala @@ -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. + */ +package org.apache.kyuubi.engine.jdbc.doris + +import org.apache.kyuubi.engine.jdbc.mysql.MySQLTRowSetGenerator + +class DorisTRowSetGenerator extends MySQLTRowSetGenerator {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQL8ConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQL8ConnectionProvider.scala new file mode 100644 index 00000000000..563d5758bdc --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQL8ConnectionProvider.scala @@ -0,0 +1,35 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider + +class MySQL8ConnectionProvider extends JdbcConnectionProvider { + + override val name: String = classOf[MySQL8ConnectionProvider].getSimpleName + + override val driverClass: String = MySQL8ConnectionProvider.driverClass + + override def canHandle(providerClass: String): Boolean = { + driverClass.equalsIgnoreCase(providerClass) + } + +} + +object MySQL8ConnectionProvider { + val driverClass: String = "com.mysql.cj.jdbc.Driver" +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLConnectionProvider.scala new file mode 100644 index 00000000000..bd57d1f53e5 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLConnectionProvider.scala @@ -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. + */ +package org.apache.kyuubi.engine.jdbc.mysql + +class MySQLConnectionProvider extends MySQL8ConnectionProvider { + + override val name: String = classOf[MySQLConnectionProvider].getSimpleName +} diff --git a/kyuubi-server/web-ui/src/router/contact/index.ts b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLSchemaHelper.scala similarity index 84% rename from kyuubi-server/web-ui/src/router/contact/index.ts rename to externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLSchemaHelper.scala index a83c653ecb9..b7351b26b3e 100644 --- a/kyuubi-server/web-ui/src/router/contact/index.ts +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLSchemaHelper.scala @@ -14,13 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.kyuubi.engine.jdbc.mysql -const routes = [ - { - path: '/contact', - name: 'contact', - component: () => import('@/views/contact/index.vue') - } -] +import org.apache.kyuubi.engine.jdbc.schema.SchemaHelper -export default routes +class MySQLSchemaHelper extends SchemaHelper {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLTRowSetGenerator.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLTRowSetGenerator.scala new file mode 100644 index 00000000000..c029131fa5a --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLTRowSetGenerator.scala @@ -0,0 +1,69 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import java.lang.{Long => JLong} +import java.sql.Types + +import org.apache.kyuubi.engine.jdbc.schema.DefaultJdbcTRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TColumnValue} + +class MySQLTRowSetGenerator extends DefaultJdbcTRowSetGenerator { + + override def toTinyIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asIntegerTColumn(rows, ordinal) + + override def toSmallIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asIntegerTColumn(rows, ordinal) + + override def toTinyIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asIntegerTColumnValue(row, ordinal) + + override def toSmallIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asIntegerTColumnValue(row, ordinal) + + override def toIntegerTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = { + val colHead = if (rows.isEmpty) None else rows.head(ordinal) + colHead match { + case _: Integer => super.toIntegerTColumn(rows, ordinal) + case _: JLong => super.toBigIntTColumn(rows, ordinal) + case _ => super.toDefaultTColumn(rows, ordinal, Types.INTEGER) + } + } + + override def toIntegerTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = { + row(ordinal) match { + case _: Integer => super.toIntegerTColumnValue(row, ordinal) + case _: JLong => super.toBigIntTColumnValue(row, ordinal) + case _ => super.toDefaultTColumnValue(row, ordinal, Types.INTEGER) + } + } + + override def toBigIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = { + val colHead = if (rows.isEmpty) None else rows.head(ordinal) + colHead match { + case _: JLong => super.toBigIntTColumn(rows, ordinal) + case _ => super.toDefaultTColumn(rows, ordinal, Types.BIGINT) + } + } + + override def toBigIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + row(ordinal) match { + case _: JLong => super.toBigIntTColumnValue(row, ordinal) + case _ => super.toDefaultTColumnValue(row, ordinal, Types.BIGINT) + } +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/ExecuteStatement.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/ExecuteStatement.scala index ef49f2b3086..4292c320b30 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/ExecuteStatement.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/ExecuteStatement.scala @@ -18,11 +18,13 @@ package org.apache.kyuubi.engine.jdbc.operation import java.sql.{Connection, Statement, Types} -import org.apache.kyuubi.Logging +import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.engine.jdbc.schema.{Column, Row, Schema} import org.apache.kyuubi.engine.jdbc.session.JdbcSessionImpl import org.apache.kyuubi.engine.jdbc.util.ResultSetWrapper -import org.apache.kyuubi.operation.{ArrayFetchIterator, IterableFetchIterator, OperationState} +import org.apache.kyuubi.operation.{ArrayFetchIterator, FetchOrientation, IterableFetchIterator, OperationState} +import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation +import org.apache.kyuubi.operation.OperationState.OperationState import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session @@ -31,12 +33,15 @@ class ExecuteStatement( override val statement: String, override val shouldRunAsync: Boolean, queryTimeout: Long, - incrementalCollect: Boolean) + incrementalCollect: Boolean, + fetchSize: Int) extends JdbcOperation(session) with Logging { private val operationLog: OperationLog = OperationLog.createOperationLog(session, getHandle) override def getOperationLog: Option[OperationLog] = Option(operationLog) + @volatile private var jdbcStatement: Statement = _ + override protected def runInternal(): Unit = { addTimeoutMonitor(queryTimeout) if (shouldRunAsync) { @@ -55,10 +60,9 @@ class ExecuteStatement( private def executeStatement(): Unit = { setState(OperationState.RUNNING) - var jdbcStatement: Statement = null try { val connection: Connection = session.asInstanceOf[JdbcSessionImpl].sessionConnection - jdbcStatement = dialect.createStatement(connection) + jdbcStatement = dialect.createStatement(connection, fetchSize) val hasResult = jdbcStatement.execute(statement) if (hasResult) { val resultSetWrapper = new ResultSetWrapper(jdbcStatement) @@ -67,9 +71,12 @@ class ExecuteStatement( iter = if (incrementalCollect) { info("Execute in incremental collect mode") - new IterableFetchIterator(resultSetWrapper.toIterable) + new IterableFetchIterator(new Iterable[Row] { + override def iterator: Iterator[Row] = resultSetWrapper + }) } else { warn(s"Execute in full collect mode") + jdbcStatement.closeOnCompletion() new ArrayFetchIterator(resultSetWrapper.toArray()) } } else { @@ -89,10 +96,27 @@ class ExecuteStatement( } catch { onError(true) } finally { - if (jdbcStatement != null) { - jdbcStatement.closeOnCompletion() - } shutdownTimeoutMonitor() } } + + override def validateFetchOrientation(order: FetchOrientation): Unit = { + if (incrementalCollect && order != FetchOrientation.FETCH_NEXT) { + throw KyuubiSQLException(s"The fetch type $order is not supported" + + " of incremental collect mode.") + } + super.validateFetchOrientation(order) + } + + override def cleanup(targetState: OperationState): Unit = withLockRequired { + try { + super.cleanup(targetState) + } finally { + if (jdbcStatement != null && !jdbcStatement.isClosed) { + jdbcStatement.close() + jdbcStatement = null + } + } + } + } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperation.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperation.scala index 2ca17375717..5e5819adb55 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperation.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperation.scala @@ -16,8 +16,6 @@ */ package org.apache.kyuubi.engine.jdbc.operation -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TRowSet} - import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.engine.jdbc.dialect.{JdbcDialect, JdbcDialects} @@ -25,6 +23,7 @@ import org.apache.kyuubi.engine.jdbc.schema.{Row, Schema} import org.apache.kyuubi.operation.{AbstractOperation, FetchIterator, OperationState} import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FETCH_PRIOR, FetchOrientation} import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TRowSet} abstract class JdbcOperation(session: Session) extends AbstractOperation(session) { @@ -36,10 +35,13 @@ abstract class JdbcOperation(session: Session) extends AbstractOperation(session protected lazy val dialect: JdbcDialect = JdbcDialects.get(conf) + def validateFetchOrientation(order: FetchOrientation): Unit = + validateDefaultFetchOrientation(order) + override def getNextRowSetInternal( order: FetchOrientation, rowSetSize: Int): TFetchResultsResp = { - validateDefaultFetchOrientation(order) + validateFetchOrientation(order) assertState(OperationState.FINISHED) setHasResultSet(true) order match { @@ -98,11 +100,8 @@ abstract class JdbcOperation(session: Session) extends AbstractOperation(session override protected def afterRun(): Unit = {} protected def toTRowSet(taken: Iterator[Row]): TRowSet = { - val rowSetHelper = dialect.getRowSetHelper() - rowSetHelper.toTRowSet( - taken.toList.map(_.values), - schema.columns, - getProtocolVersion) + dialect.getTRowSetGenerator() + .toTRowSet(taken.toSeq.map(_.values), schema.columns, getProtocolVersion) } override def getResultSetMetadata: TGetResultSetMetadataResp = { diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperationManager.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperationManager.scala index d10bb34cfb6..7ced3e6b87c 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperationManager.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/operation/JdbcOperationManager.scala @@ -20,7 +20,7 @@ import java.util import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.OPERATION_INCREMENTAL_COLLECT +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_JDBC_FETCH_SIZE, OPERATION_INCREMENTAL_COLLECT} import org.apache.kyuubi.engine.jdbc.dialect.{JdbcDialect, JdbcDialects} import org.apache.kyuubi.engine.jdbc.session.JdbcSessionImpl import org.apache.kyuubi.engine.jdbc.util.SupportServiceLoader @@ -44,13 +44,16 @@ class JdbcOperationManager(conf: KyuubiConf) extends OperationManager("JdbcOpera val incrementalCollect = normalizedConf.get(OPERATION_INCREMENTAL_COLLECT.key).map( _.toBoolean).getOrElse( session.sessionManager.getConf.get(OPERATION_INCREMENTAL_COLLECT)) + val fetchSize = normalizedConf.get(ENGINE_JDBC_FETCH_SIZE.key).map(_.toInt) + .getOrElse(session.sessionManager.getConf.get(ENGINE_JDBC_FETCH_SIZE)) val executeStatement = new ExecuteStatement( session, statement, runAsync, queryTimeout, - incrementalCollect) + incrementalCollect, + fetchSize) addOperation(executeStatement) } @@ -60,16 +63,26 @@ class JdbcOperationManager(conf: KyuubiConf) extends OperationManager("JdbcOpera } override def newGetCatalogsOperation(session: Session): Operation = { - val operation = dialect.getCatalogsOperation(session) - addOperation(operation) + val query = dialect.getCatalogsOperation() + val normalizedConf = session.asInstanceOf[JdbcSessionImpl].normalizedConf + val fetchSize = normalizedConf.get(ENGINE_JDBC_FETCH_SIZE.key).map(_.toInt) + .getOrElse(session.sessionManager.getConf.get(ENGINE_JDBC_FETCH_SIZE)) + val executeStatement = + new ExecuteStatement(session, query, false, 0L, true, fetchSize) + addOperation(executeStatement) } override def newGetSchemasOperation( session: Session, catalog: String, schema: String): Operation = { - val operation = dialect.getSchemasOperation(session) - addOperation(operation) + val query = dialect.getSchemasOperation(catalog, schema) + val normalizedConf = session.asInstanceOf[JdbcSessionImpl].normalizedConf + val fetchSize = normalizedConf.get(ENGINE_JDBC_FETCH_SIZE.key).map(_.toInt) + .getOrElse(session.sessionManager.getConf.get(ENGINE_JDBC_FETCH_SIZE)) + val executeStatement = + new ExecuteStatement(session, query, false, 0L, true, fetchSize) + addOperation(executeStatement) } override def newGetTablesOperation( @@ -79,8 +92,11 @@ class JdbcOperationManager(conf: KyuubiConf) extends OperationManager("JdbcOpera tableName: String, tableTypes: util.List[String]): Operation = { val query = dialect.getTablesQuery(catalogName, schemaName, tableName, tableTypes) + val normalizedConf = session.asInstanceOf[JdbcSessionImpl].normalizedConf + val fetchSize = normalizedConf.get(ENGINE_JDBC_FETCH_SIZE.key).map(_.toInt) + .getOrElse(session.sessionManager.getConf.get(ENGINE_JDBC_FETCH_SIZE)) val executeStatement = - new ExecuteStatement(session, query, false, 0L, true) + new ExecuteStatement(session, query, false, 0L, true, fetchSize) addOperation(executeStatement) } @@ -96,8 +112,12 @@ class JdbcOperationManager(conf: KyuubiConf) extends OperationManager("JdbcOpera tableName: String, columnName: String): Operation = { val query = dialect.getColumnsQuery(session, catalogName, schemaName, tableName, columnName) + val normalizedConf = session.asInstanceOf[JdbcSessionImpl].normalizedConf + val fetchSize = normalizedConf.get(ENGINE_JDBC_FETCH_SIZE.key).map( + _.toInt).getOrElse( + session.sessionManager.getConf.get(ENGINE_JDBC_FETCH_SIZE)) val executeStatement = - new ExecuteStatement(session, query, false, 0L, true) + new ExecuteStatement(session, query, false, 0L, true, fetchSize) addOperation(executeStatement) } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixRowSetHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixTRowSetGenerator.scala similarity index 85% rename from externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixRowSetHelper.scala rename to externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixTRowSetGenerator.scala index 67d9d09e529..f8740fce483 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixRowSetHelper.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/phoenix/PhoenixTRowSetGenerator.scala @@ -16,6 +16,6 @@ */ package org.apache.kyuubi.engine.jdbc.phoenix -import org.apache.kyuubi.engine.jdbc.schema.RowSetHelper +import org.apache.kyuubi.engine.jdbc.schema.DefaultJdbcTRowSetGenerator -class PhoenixRowSetHelper extends RowSetHelper {} +class PhoenixTRowSetGenerator extends DefaultJdbcTRowSetGenerator {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/Mysql8ConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLConnectionProvider.scala similarity index 79% rename from externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/Mysql8ConnectionProvider.scala rename to externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLConnectionProvider.scala index 8dc930e4889..3fb392795d1 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/mysql/Mysql8ConnectionProvider.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLConnectionProvider.scala @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.kyuubi.engine.jdbc.mysql +package org.apache.kyuubi.engine.jdbc.postgresql import org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider -class Mysql8ConnectionProvider extends JdbcConnectionProvider { +class PostgreSQLConnectionProvider extends JdbcConnectionProvider { - override val name: String = classOf[Mysql8ConnectionProvider].getSimpleName + override val name: String = classOf[PostgreSQLConnectionProvider].getSimpleName - override val driverClass: String = "com.mysql.cj.jdbc.Driver" + override val driverClass: String = "org.postgresql.Driver" override def canHandle(providerClass: String): Boolean = { driverClass.equalsIgnoreCase(providerClass) diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLSchemaHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLSchemaHelper.scala new file mode 100644 index 00000000000..47ad314d33e --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLSchemaHelper.scala @@ -0,0 +1,25 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import org.apache.kyuubi.engine.jdbc.schema.SchemaHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class PostgreSQLSchemaHelper extends SchemaHelper { + + override def smallIntToTTypeId: TTypeId = TTypeId.INT_TYPE +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLTRowSetGenerator.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLTRowSetGenerator.scala new file mode 100644 index 00000000000..104b3b15dde --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLTRowSetGenerator.scala @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import org.apache.kyuubi.engine.jdbc.schema.DefaultJdbcTRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TColumnValue} + +class PostgreSQLTRowSetGenerator extends DefaultJdbcTRowSetGenerator { + + override def toSmallIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + toIntegerTColumn(rows, ordinal) + + override def toSmallIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + toIntegerTColumnValue(row, ordinal) +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/DefaultJdbcTRowSetGenerator.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/DefaultJdbcTRowSetGenerator.scala new file mode 100644 index 00000000000..2c9ddd6da3e --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/DefaultJdbcTRowSetGenerator.scala @@ -0,0 +1,129 @@ +/* + * 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.kyuubi.engine.jdbc.schema + +import java.sql.Date +import java.sql.Types._ +import java.time.LocalDateTime + +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.util.RowSetUtils.{formatDate, formatLocalDateTime} + +class DefaultJdbcTRowSetGenerator extends JdbcTRowSetGenerator { + + override def toTColumn(rows: Seq[Seq[_]], ordinal: Int, sqlType: Int): TColumn = + sqlType match { + case BIT => toBitTColumn(rows, ordinal) + case TINYINT => toTinyIntTColumn(rows, ordinal) + case SMALLINT => toSmallIntTColumn(rows, ordinal) + case INTEGER => toIntegerTColumn(rows, ordinal) + case BIGINT => toBigIntTColumn(rows, ordinal) + case REAL => toRealTColumn(rows, ordinal) + case DOUBLE => toDoubleTColumn(rows, ordinal) + case CHAR => toCharTColumn(rows, ordinal) + case VARCHAR => toVarcharTColumn(rows, ordinal) + case _ => toDefaultTColumn(rows, ordinal, sqlType) + } + + override def toTColumnValue(row: Seq[_], ordinal: Int, types: Seq[Column]): TColumnValue = { + getColumnType(types, ordinal) match { + case BIT => toBitTColumnValue(row, ordinal) + case TINYINT => toTinyIntTColumnValue(row, ordinal) + case SMALLINT => toSmallIntTColumnValue(row, ordinal) + case INTEGER => toIntegerTColumnValue(row, ordinal) + case BIGINT => toBigIntTColumnValue(row, ordinal) + case REAL => toRealTColumnValue(row, ordinal) + case DOUBLE => toDoubleTColumnValue(row, ordinal) + case CHAR => toCharTColumnValue(row, ordinal) + case VARCHAR => toVarcharTColumnValue(row, ordinal) + case otherType => toDefaultTColumnValue(row, ordinal, otherType) + } + } + + def toDefaultTColumn(rows: Seq[Seq[_]], ordinal: Int, sqlType: Int): TColumn = + asStringTColumn( + rows, + ordinal, + convertFunc = (row, ordinal) => toHiveString(row(ordinal), sqlType)) + + def toBitTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asBooleanTColumn(rows, ordinal) + + def toTinyIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asShortTColumn(rows, ordinal) + + def toSmallIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asShortTColumn(rows, ordinal) + + def toIntegerTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asIntegerTColumn(rows, ordinal) + + def toBigIntTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asLongTColumn(rows, ordinal) + + def toRealTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asFloatTColumn(rows, ordinal) + + def toDoubleTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asDoubleTColumn(rows, ordinal) + + def toCharTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asStringTColumn(rows, ordinal) + + def toVarcharTColumn(rows: Seq[Seq[_]], ordinal: Int): TColumn = + asStringTColumn(rows, ordinal) + + // ========================================================== + + def toBitTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asBooleanTColumnValue(row, ordinal) + + def toTinyIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asShortTColumnValue(row, ordinal) + + def toSmallIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asShortTColumnValue(row, ordinal) + + def toIntegerTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asIntegerTColumnValue(row, ordinal) + + def toBigIntTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asLongTColumnValue(row, ordinal) + + def toRealTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asFloatTColumnValue(row, ordinal) + + def toDoubleTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asDoubleTColumnValue(row, ordinal) + + def toCharTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asStringTColumnValue(row, ordinal) + + def toVarcharTColumnValue(row: Seq[_], ordinal: Int): TColumnValue = + asStringTColumnValue(row, ordinal) + + def toDefaultTColumnValue(row: Seq[_], ordinal: Int, sqlType: Int): TColumnValue = + asStringTColumnValue(row, ordinal, rawValue => toHiveString(rawValue, sqlType)) + + def toHiveString(data: Any, sqlType: Int): String = + (data, sqlType) match { + case (date: Date, DATE) => formatDate(date) + case (dateTime: LocalDateTime, TIMESTAMP) => formatLocalDateTime(dateTime) + case (decimal: java.math.BigDecimal, DECIMAL) => decimal.toPlainString + case (other, _) => other.toString + } +} diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PermanentViewMarker.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/JdbcTRowSetGenerator.scala similarity index 58% rename from extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PermanentViewMarker.scala rename to externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/JdbcTRowSetGenerator.scala index d19f7a92314..233a6a79946 100644 --- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/util/PermanentViewMarker.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/JdbcTRowSetGenerator.scala @@ -14,22 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.kyuubi.engine.jdbc.schema -package org.apache.kyuubi.plugin.spark.authz.util +import org.apache.kyuubi.engine.result.TRowSetGenerator -import org.apache.spark.sql.catalyst.catalog.CatalogTable -import org.apache.spark.sql.catalyst.expressions.Attribute -import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode} +trait JdbcTRowSetGenerator extends TRowSetGenerator[Seq[Column], Seq[_], Int] { + override def getColumnSizeFromSchemaType(schema: Seq[Column]): Int = schema.length -case class PermanentViewMarker( - child: LogicalPlan, - catalogTable: CatalogTable, - visitColNames: Seq[String]) extends UnaryNode - with WithInternalChild { + override def getColumnType(schema: Seq[Column], ordinal: Int): Int = schema(ordinal).sqlType - override def output: Seq[Attribute] = child.output - - override def withNewChildInternal(newChild: LogicalPlan): LogicalPlan = - copy(child = newChild) + override protected def isColumnNullAt(row: Seq[_], ordinal: Int): Boolean = row(ordinal) == null + override protected def getColumnAs[T](row: Seq[_], ordinal: Int): T = row(ordinal).asInstanceOf[T] } diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/RowSetHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/RowSetHelper.scala deleted file mode 100644 index 74b4cec108d..00000000000 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/RowSetHelper.scala +++ /dev/null @@ -1,323 +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. - */ -package org.apache.kyuubi.engine.jdbc.schema - -import java.{lang, util} -import java.sql.{Date, Types} -import java.time.LocalDateTime - -import scala.collection.JavaConverters._ - -import org.apache.hive.service.rpc.thrift._ - -import org.apache.kyuubi.util.RowSetUtils.{bitSetToBuffer, formatDate, formatLocalDateTime} - -abstract class RowSetHelper { - - def toTRowSet( - rows: Seq[List[_]], - columns: List[Column], - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBasedSet(rows, columns) - } else { - toColumnBasedSet(rows, columns) - } - } - - private def toRowBasedSet(rows: Seq[List[_]], columns: List[Column]): TRowSet = { - val rowSize = rows.length - val tRows = new util.ArrayList[TRow](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - val columnSize = row.size - var j = 0 - while (j < columnSize) { - val columnValue = toTColumnValue(j, row, columns) - tRow.addToColVals(columnValue) - j += 1 - } - tRows.add(tRow) - i += 1 - } - new TRowSet(0, tRows) - } - - private def toColumnBasedSet(rows: Seq[List[_]], columns: List[Column]): TRowSet = { - val size = rows.size - val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](size)) - val columnSize = columns.length - var i = 0 - while (i < columnSize) { - val field = columns(i) - val tColumn = toTColumn(rows, i, field.sqlType) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - protected def toTColumn( - rows: Seq[Seq[Any]], - ordinal: Int, - sqlType: Int): TColumn = { - sqlType match { - case Types.BIT => - toBitTColumn(rows, ordinal) - - case Types.TINYINT => - toTinyIntTColumn(rows, ordinal) - - case Types.SMALLINT => - toSmallIntTColumn(rows, ordinal) - - case Types.INTEGER => - toIntegerTColumn(rows, ordinal) - - case Types.BIGINT => - toBigIntTColumn(rows, ordinal) - - case Types.REAL => - toRealTColumn(rows, ordinal) - - case Types.DOUBLE => - toDoubleTColumn(rows, ordinal) - - case Types.CHAR => - toCharTColumn(rows, ordinal) - - case Types.VARCHAR => - toVarcharTColumn(rows, ordinal) - - case _ => - toDefaultTColumn(rows, ordinal, sqlType) - } - } - - protected def toTColumnValue(ordinal: Int, row: List[Any], types: List[Column]): TColumnValue = { - types(ordinal).sqlType match { - case Types.BIT => - toBitTColumnValue(row, ordinal) - - case Types.TINYINT => - toTinyIntTColumnValue(row, ordinal) - - case Types.SMALLINT => - toSmallIntTColumnValue(row, ordinal) - - case Types.INTEGER => - toIntegerTColumnValue(row, ordinal) - - case Types.BIGINT => - toBigIntTColumnValue(row, ordinal) - - case Types.REAL => - toRealTColumnValue(row, ordinal) - - case Types.DOUBLE => - toDoubleTColumnValue(row, ordinal) - - case Types.CHAR => - toCharTColumnValue(row, ordinal) - - case Types.VARCHAR => - toVarcharTColumnValue(row, ordinal) - - case _ => - toDefaultTColumnValue(row, ordinal, types) - } - } - - protected def getOrSetAsNull[T]( - rows: Seq[Seq[Any]], - ordinal: Int, - nulls: java.util.BitSet, - defaultVal: T): java.util.List[T] = { - val size = rows.length - val ret = new java.util.ArrayList[T](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row(ordinal) == null - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row(ordinal).asInstanceOf[T]) - } - idx += 1 - } - ret - } - - protected def toDefaultTColumn(rows: Seq[Seq[Any]], ordinal: Int, sqlType: Int): TColumn = { - val nulls = new java.util.BitSet() - val rowSize = rows.length - val values = new util.ArrayList[String](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - nulls.set(i, row(ordinal) == null) - val value = - if (row(ordinal) == null) { - "" - } else { - toHiveString(row(ordinal), sqlType) - } - values.add(value) - i += 1 - } - TColumn.stringVal(new TStringColumn(values, nulls)) - } - - protected def toBitTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[java.lang.Boolean](rows, ordinal, nulls, true) - TColumn.boolVal(new TBoolColumn(values, nulls)) - } - - protected def toTinyIntTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[java.lang.Byte](rows, ordinal, nulls, 0.toByte) - TColumn.byteVal(new TByteColumn(values, nulls)) - } - - protected def toSmallIntTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[java.lang.Short](rows, ordinal, nulls, 0.toShort) - TColumn.i16Val(new TI16Column(values, nulls)) - } - - protected def toIntegerTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[java.lang.Integer](rows, ordinal, nulls, 0) - TColumn.i32Val(new TI32Column(values, nulls)) - } - - protected def toBigIntTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[lang.Long](rows, ordinal, nulls, 0L) - TColumn.i64Val(new TI64Column(values, nulls)) - } - - protected def toRealTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[lang.Float](rows, ordinal, nulls, 0.toFloat) - .asScala.map(n => java.lang.Double.valueOf(n.toString)).asJava - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - } - - protected def toDoubleTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[lang.Double](rows, ordinal, nulls, 0.toDouble) - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - } - - protected def toCharTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - toVarcharTColumn(rows, ordinal) - } - - protected def toVarcharTColumn(rows: Seq[Seq[Any]], ordinal: Int): TColumn = { - val nulls = new java.util.BitSet() - val values = getOrSetAsNull[String](rows, ordinal, nulls, "") - TColumn.stringVal(new TStringColumn(values, nulls)) - } - - // ========================================================== - - protected def toBitTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val boolValue = new TBoolValue - if (row(ordinal) != null) boolValue.setValue(row(ordinal).asInstanceOf[Boolean]) - TColumnValue.boolVal(boolValue) - } - - protected def toTinyIntTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val byteValue = new TByteValue - if (row(ordinal) != null) byteValue.setValue(row(ordinal).asInstanceOf[Byte]) - TColumnValue.byteVal(byteValue) - } - - protected def toSmallIntTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tI16Value = new TI16Value - if (row(ordinal) != null) tI16Value.setValue(row(ordinal).asInstanceOf[Short]) - TColumnValue.i16Val(tI16Value) - } - - protected def toIntegerTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tI32Value = new TI32Value - if (row(ordinal) != null) tI32Value.setValue(row(ordinal).asInstanceOf[Int]) - TColumnValue.i32Val(tI32Value) - } - - protected def toBigIntTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tI64Value = new TI64Value - if (row(ordinal) != null) tI64Value.setValue(row(ordinal).asInstanceOf[Long]) - TColumnValue.i64Val(tI64Value) - } - - protected def toRealTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tDoubleValue = new TDoubleValue - if (row(ordinal) != null) { - val doubleValue = java.lang.Double.valueOf(row(ordinal).asInstanceOf[Float].toString) - tDoubleValue.setValue(doubleValue) - } - TColumnValue.doubleVal(tDoubleValue) - } - - protected def toDoubleTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tDoubleValue = new TDoubleValue - if (row(ordinal) != null) tDoubleValue.setValue(row(ordinal).asInstanceOf[Double]) - TColumnValue.doubleVal(tDoubleValue) - } - - protected def toCharTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - toVarcharTColumnValue(row, ordinal) - } - - protected def toVarcharTColumnValue(row: List[Any], ordinal: Int): TColumnValue = { - val tStringValue = new TStringValue - if (row(ordinal) != null) tStringValue.setValue(row(ordinal).asInstanceOf[String]) - TColumnValue.stringVal(tStringValue) - } - - protected def toDefaultTColumnValue( - row: List[Any], - ordinal: Int, - types: List[Column]): TColumnValue = { - val tStrValue = new TStringValue - if (row(ordinal) != null) { - tStrValue.setValue( - toHiveString(row(ordinal), types(ordinal).sqlType)) - } - TColumnValue.stringVal(tStrValue) - } - - protected def toHiveString(data: Any, sqlType: Int): String = { - (data, sqlType) match { - case (date: Date, Types.DATE) => - formatDate(date) - case (dateTime: LocalDateTime, Types.TIMESTAMP) => - formatLocalDateTime(dateTime) - case (decimal: java.math.BigDecimal, Types.DECIMAL) => - decimal.toPlainString - case (other, _) => - other.toString - } - } -} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/SchemaHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/SchemaHelper.scala index 455eb2a9224..6b39bb3dbe4 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/SchemaHelper.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/schema/SchemaHelper.scala @@ -21,7 +21,7 @@ import java.util.Collections import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ abstract class SchemaHelper { diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionImpl.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionImpl.scala index 8b36e5a56df..09d08d2c896 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionImpl.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionImpl.scala @@ -20,8 +20,6 @@ import java.sql.{Connection, DatabaseMetaData} import scala.util.{Failure, Success, Try} -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ @@ -29,6 +27,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider import org.apache.kyuubi.engine.jdbc.util.KyuubiJdbcUtils import org.apache.kyuubi.session.{AbstractSession, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} class JdbcSessionImpl( protocol: TProtocolVersion, diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionManager.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionManager.scala index 09958e0507f..513e61303fd 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionManager.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/session/JdbcSessionManager.scala @@ -16,8 +16,6 @@ */ package org.apache.kyuubi.engine.jdbc.session -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.ENGINE_SHARE_LEVEL import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY @@ -26,6 +24,7 @@ import org.apache.kyuubi.engine.jdbc.JdbcSQLEngine import org.apache.kyuubi.engine.jdbc.operation.JdbcOperationManager import org.apache.kyuubi.operation.OperationManager import org.apache.kyuubi.session.{Session, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class JdbcSessionManager(name: String) extends SessionManager(name) { diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksConnectionProvider.scala new file mode 100644 index 00000000000..09b7efb3ff5 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksConnectionProvider.scala @@ -0,0 +1,24 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.engine.jdbc.mysql.MySQL8ConnectionProvider + +class StarRocksConnectionProvider extends MySQL8ConnectionProvider { + + override val name: String = classOf[StarRocksConnectionProvider].getSimpleName +} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSchemaHelper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSchemaHelper.scala new file mode 100644 index 00000000000..e6b4e152140 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSchemaHelper.scala @@ -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. + */ +package org.apache.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.engine.jdbc.mysql.MySQLSchemaHelper + +class StarRocksSchemaHelper extends MySQLSchemaHelper {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksTRowSetGenerator.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksTRowSetGenerator.scala new file mode 100644 index 00000000000..736ce766461 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksTRowSetGenerator.scala @@ -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. + */ +package org.apache.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.engine.jdbc.mysql.MySQLTRowSetGenerator + +class StarRocksTRowSetGenerator extends MySQLTRowSetGenerator {} diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/util/ResultSetWrapper.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/util/ResultSetWrapper.scala index 8bc7027f19b..0fead73b1a6 100644 --- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/util/ResultSetWrapper.scala +++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/util/ResultSetWrapper.scala @@ -30,6 +30,7 @@ class ResultSetWrapper(statement: Statement) private lazy val metadata = currentResult.getMetaData override def hasNext: Boolean = { + if (currentResult == null) return false val result = currentResult.next() if (!result) { val hasMoreResults = statement.getMoreResults(Statement.CLOSE_CURRENT_RESULT) @@ -37,6 +38,7 @@ class ResultSetWrapper(statement: Statement) currentResult = statement.getResultSet currentResult.next() } else { + currentResult = null false } } else { diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/CheckJdbcDialectSPISuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/CheckJdbcDialectSPISuite.scala new file mode 100644 index 00000000000..e30ebce5d73 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/CheckJdbcDialectSPISuite.scala @@ -0,0 +1,41 @@ +/* + * 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.kyuubi.engine.jdbc + +import java.nio.file.Paths + +// scalastyle:off +import org.scalatest.funsuite.AnyFunSuite + +import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ + +class CheckJdbcDialectSPISuite extends AnyFunSuite { + // scalastyle:on + + test("check JDBC dialect SPI service file sorted") { + Seq( + "org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider", + "org.apache.kyuubi.engine.jdbc.dialect.JdbcDialect") + .foreach { fileName => + val filePath = Paths.get( + s"${getCurrentModuleHome(this)}/src/main/resources/META-INF/services/$fileName") + assertFileContentSorted(filePath) + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/WithJdbcServerContainer.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/WithJdbcServerContainer.scala index 18c2316c1bc..89b5534be58 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/WithJdbcServerContainer.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/WithJdbcServerContainer.scala @@ -16,8 +16,8 @@ */ package org.apache.kyuubi.engine.jdbc -import com.dimafeng.testcontainers.ForAllTestContainer +import com.dimafeng.testcontainers.scalatest.TestContainerForAll import org.apache.kyuubi.KyuubiFunSuite -trait WithJdbcServerContainer extends KyuubiFunSuite with ForAllTestContainer {} +trait WithJdbcServerContainer extends KyuubiFunSuite with TestContainerForAll {} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/OperationWithEngineSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/OperationWithEngineSuite.scala index d5e3f4f0fcc..31ca4dee737 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/OperationWithEngineSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/OperationWithEngineSuite.scala @@ -16,17 +16,16 @@ */ package org.apache.kyuubi.engine.jdbc.doris -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TGetInfoReq, TGetInfoType, TStatusCode} - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TGetInfoReq, TGetInfoType, TStatusCode} class OperationWithEngineSuite extends DorisOperationSuite with HiveJDBCTestHelper { override protected def jdbcUrl: String = jdbcConnectionUrl - test("Test for Jdbc engine getInfo") { + test("doris - test for Jdbc engine getInfo") { val metaData = ConnectionProvider.create(kyuubiConf).getMetaData withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { @@ -60,7 +59,7 @@ class OperationWithEngineSuite extends DorisOperationSuite with HiveJDBCTestHelp } } - test("JDBC ExecuteStatement operation should contain operationLog") { + test("doris - JDBC ExecuteStatement operation should contain operationLog") { withSessionHandle { (client, handle) => val tExecuteStatementReq = new TExecuteStatementReq() tExecuteStatementReq.setSessionHandle(handle) diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/SessionSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/SessionSuite.scala index a8204105f7e..b5af0829a60 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/SessionSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/SessionSuite.scala @@ -20,7 +20,7 @@ import org.apache.kyuubi.operation.HiveJDBCTestHelper class SessionSuite extends WithDorisEngine with HiveJDBCTestHelper { - test("test session") { + test("doris - test session") { withJdbcStatement() { statement => val resultSet = statement.executeQuery( "select '1' as id") diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/StatementSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/StatementSuite.scala index 663c0da3abb..b27ad880b34 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/StatementSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/StatementSuite.scala @@ -22,7 +22,7 @@ import org.apache.kyuubi.operation.HiveJDBCTestHelper class StatementSuite extends WithDorisEngine with HiveJDBCTestHelper { - test("test select") { + test("doris - test select") { withJdbcStatement("test1") { statement => statement.execute("create database if not exists db1") statement.execute("use db1") @@ -44,7 +44,7 @@ class StatementSuite extends WithDorisEngine with HiveJDBCTestHelper { } } - test("test types") { + test("doris - test types") { withJdbcStatement("test1") { statement => statement.execute("create database if not exists db1") statement.execute("use db1") diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisContainer.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisContainer.scala index 8092e329941..c37478e9989 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisContainer.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisContainer.scala @@ -27,15 +27,13 @@ import org.apache.kyuubi.engine.jdbc.WithJdbcServerContainer trait WithDorisContainer extends WithJdbcServerContainer { - private val DORIS_FE_PORT = 9030 - - private val DORIS_BE_PORT = 8040 + private val DORIS_FE_MYSQL_PORT = 9030 + private val DORIS_BE_HTTTP_PORT = 8040 private val DORIS_FE_SERVICE_NAME = "doris-fe" - private val DORIS_BE_SERVICE_NAME = "doris-be" - override val container: DockerComposeContainer = + override val containerDef: DockerComposeContainer.Def = DockerComposeContainer .Def( composeFiles = new File(Utils.getContextOrKyuubiClassLoader @@ -43,25 +41,18 @@ trait WithDorisContainer extends WithJdbcServerContainer { exposedServices = Seq[ExposedService]( ExposedService( DORIS_FE_SERVICE_NAME, - DORIS_FE_PORT, + DORIS_FE_MYSQL_PORT, waitStrategy = new DockerHealthcheckWaitStrategy().withStartupTimeout(Duration.ofMinutes(5))), ExposedService( DORIS_BE_SERVICE_NAME, - DORIS_BE_PORT, + DORIS_BE_HTTTP_PORT, waitStrategy = new DockerHealthcheckWaitStrategy().withStartupTimeout(Duration.ofMinutes(5))))) - .createContainer() - - protected def feUrl: String = { - val feHost: String = container.getServiceHost(DORIS_FE_SERVICE_NAME, DORIS_FE_PORT) - val fePort: Int = container.getServicePort(DORIS_FE_SERVICE_NAME, DORIS_FE_PORT) - val url = s"$feHost:$fePort" - url - } - override def afterAll(): Unit = { - super.afterAll() - container.close() + protected def feJdbcUrl: String = withContainers { container => + val feHost: String = container.getServiceHost(DORIS_FE_SERVICE_NAME, DORIS_FE_MYSQL_PORT) + val fePort: Int = container.getServicePort(DORIS_FE_SERVICE_NAME, DORIS_FE_MYSQL_PORT) + s"jdbc:mysql://$feHost:$fePort" } } diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisEngine.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisEngine.scala index 9945fb64047..692f37b9515 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisEngine.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/doris/WithDorisEngine.scala @@ -23,7 +23,7 @@ trait WithDorisEngine extends WithJdbcEngine with WithDorisContainer { override def withKyuubiConf: Map[String, String] = Map( ENGINE_SHARE_LEVEL.key -> "SERVER", - ENGINE_JDBC_CONNECTION_URL.key -> s"jdbc:mysql://$feUrl", + ENGINE_JDBC_CONNECTION_URL.key -> feJdbcUrl, ENGINE_JDBC_CONNECTION_USER.key -> "root", ENGINE_JDBC_CONNECTION_PASSWORD.key -> "", ENGINE_TYPE.key -> "jdbc", diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLOperationSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLOperationSuite.scala new file mode 100644 index 00000000000..ffd7c0a0fe8 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/MySQLOperationSuite.scala @@ -0,0 +1,253 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import java.sql.ResultSet + +import scala.collection.mutable.ArrayBuffer + +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ + +abstract class MySQLOperationSuite extends WithMySQLEngine with HiveJDBCTestHelper { + test("mysql - get tables") { + case class Table(catalog: String, schema: String, tableName: String, tableType: String) + + withJdbcStatement() { statement => + val meta = statement.getConnection.getMetaData + val resultBuffer = ArrayBuffer[Table]() + + var tables = meta.getTables(null, null, null, null) + while (tables.next()) { + resultBuffer += + Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "information_schema", "TABLES", "SYSTEM VIEW"))) + assert(resultBuffer.contains(Table("def", "information_schema", "VIEWS", "SYSTEM VIEW"))) + resultBuffer.clear() + + statement.execute("create database if not exists db1") + statement.execute("create table db1.test1(id bigint)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + statement.execute("create table db1.test2(id bigint)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + + statement.execute("create database if not exists db2") + statement.execute("create table db2.test1(id bigint)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + statement.execute("create table db2.test2(id bigint)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + + statement.execute("create view db1.view1 (k1) as select id from db1.test1") + + tables = meta.getTables(null, "db1", "test1", Array("BASE TABLE")) + while (tables.next()) { + val table = Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + assert(table == Table("def", "db1", "test1", "BASE TABLE")) + } + + tables = meta.getTables("def", "db1", null, null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, null, "test1", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "db%", "test1", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "db2", "test%", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "fake_db", "test1", null) + assert(!tables.next()) + + tables = meta.getTables(null, "db1", null, Array("VIEW")) + while (tables.next()) { + val table = Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + assert(table == Table("def", "db1", "view1", "VIEW")) + } + + tables = meta.getTables(null, null, null, Array("VIEW", "BASE TABLE")) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db1", "test2", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test2", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db1", "view1", "VIEW"))) + resultBuffer.clear() + + statement.execute("drop view db1.view1") + statement.execute("drop table db1.test1") + statement.execute("drop table db1.test2") + statement.execute("drop table db2.test1") + statement.execute("drop table db2.test2") + statement.execute("drop database db1") + statement.execute("drop database db2") + } + } + + test("mysql - get columns") { + case class Column(tableSchema: String, tableName: String, columnName: String) + + def buildColumn(resultSet: ResultSet): Column = { + val schema = resultSet.getString(TABLE_SCHEMA) + val tableName = resultSet.getString(TABLE_NAME) + val columnName = resultSet.getString(COLUMN_NAME) + val column = Column(schema, tableName, columnName) + column + } + + withJdbcStatement() { statement => + val metadata = statement.getConnection.getMetaData + statement.execute("create database if not exists db1") + statement.execute("create table if not exists db1.test1" + + "(id bigint, str1 varchar(255), str2 varchar(255), age int)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + statement.execute("create table if not exists db1.test2" + + "(id bigint, str1 varchar(255), str2 varchar(255), age int)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + + statement.execute("create database if not exists db2") + + statement.execute("create table if not exists db2.test1" + + "(id bigint, str1 varchar(255), str2 varchar(255), age int)" + + "ENGINE=InnoDB DEFAULT CHARSET=utf8;") + + val resultBuffer = ArrayBuffer[Column]() + val resultSet1 = metadata.getColumns(null, "db1", null, null) + while (resultSet1.next()) { + val column = buildColumn(resultSet1) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "id"))) + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + + assert(resultBuffer.contains(Column("db1", "test2", "id"))) + assert(resultBuffer.contains(Column("db1", "test2", "str1"))) + assert(resultBuffer.contains(Column("db1", "test2", "str2"))) + assert(resultBuffer.contains(Column("db1", "test2", "age"))) + + resultBuffer.clear() + + val resultSet2 = metadata.getColumns(null, null, "test1", null) + while (resultSet2.next()) { + val column = buildColumn(resultSet2) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "id"))) + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + + assert(resultBuffer.contains(Column("db2", "test1", "id"))) + assert(resultBuffer.contains(Column("db2", "test1", "str1"))) + assert(resultBuffer.contains(Column("db2", "test1", "str2"))) + assert(resultBuffer.contains(Column("db2", "test1", "age"))) + + resultBuffer.clear() + + val resultSet3 = metadata.getColumns(null, null, null, "age") + while (resultSet3.next()) { + val column = buildColumn(resultSet3) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + assert(resultBuffer.contains(Column("db1", "test2", "age"))) + assert(resultBuffer.contains(Column("db2", "test1", "age"))) + + resultBuffer.clear() + + val resultSet4 = metadata.getColumns(null, "d%1", "t%1", "str%") + while (resultSet4.next()) { + val column = buildColumn(resultSet4) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + + resultBuffer.clear() + + val resultSet5 = metadata.getColumns(null, "d%1", "t%1", "fake") + assert(!resultSet5.next()) + + statement.execute("drop table db1.test1") + statement.execute("drop table db1.test2") + statement.execute("drop database db1") + statement.execute("drop table db2.test1") + statement.execute("drop database db2") + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/OperationWithEngineSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/OperationWithEngineSuite.scala new file mode 100644 index 00000000000..b8264c06992 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/OperationWithEngineSuite.scala @@ -0,0 +1,78 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class OperationWithEngineSuite extends MySQLOperationSuite with HiveJDBCTestHelper { + + override protected def jdbcUrl: String = jdbcConnectionUrl + + test("Test for Jdbc engine getInfo") { + val metaData = ConnectionProvider.create(kyuubiConf).getMetaData + + withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { + withSessionHandle { (client, handle) => + val req = new TGetInfoReq() + req.setSessionHandle(handle) + req.setInfoType(TGetInfoType.CLI_DBMS_NAME) + assert(client.GetInfo(req).getInfoValue.getStringValue == metaData.getDatabaseProductName) + + val req2 = new TGetInfoReq() + req2.setSessionHandle(handle) + req2.setInfoType(TGetInfoType.CLI_DBMS_VER) + assert( + client.GetInfo(req2).getInfoValue.getStringValue == metaData.getDatabaseProductVersion) + + val req3 = new TGetInfoReq() + req3.setSessionHandle(handle) + req3.setInfoType(TGetInfoType.CLI_MAX_COLUMN_NAME_LEN) + assert(client.GetInfo(req3).getInfoValue.getLenValue == metaData.getMaxColumnNameLength) + + val req4 = new TGetInfoReq() + req4.setSessionHandle(handle) + req4.setInfoType(TGetInfoType.CLI_MAX_SCHEMA_NAME_LEN) + assert(client.GetInfo(req4).getInfoValue.getLenValue == metaData.getMaxSchemaNameLength) + + val req5 = new TGetInfoReq() + req5.setSessionHandle(handle) + req5.setInfoType(TGetInfoType.CLI_MAX_TABLE_NAME_LEN) + assert(client.GetInfo(req5).getInfoValue.getLenValue == metaData.getMaxTableNameLength) + } + } + } + + test("JDBC ExecuteStatement operation should contain operationLog") { + withSessionHandle { (client, handle) => + val tExecuteStatementReq = new TExecuteStatementReq() + tExecuteStatementReq.setSessionHandle(handle) + tExecuteStatementReq.setStatement("SELECT 1") + val tExecuteStatementResp = client.ExecuteStatement(tExecuteStatementReq) + + val tFetchResultsReq = new TFetchResultsReq() + tFetchResultsReq.setOperationHandle(tExecuteStatementResp.getOperationHandle) + tFetchResultsReq.setFetchType(1) + tFetchResultsReq.setMaxRows(1) + + val tFetchResultsResp = client.FetchResults(tFetchResultsReq) + assert(tFetchResultsResp.getStatus.getStatusCode === TStatusCode.SUCCESS_STATUS) + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/SessionSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/SessionSuite.scala new file mode 100644 index 00000000000..65107603d77 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/SessionSuite.scala @@ -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 org.apache.kyuubi.engine.jdbc.mysql + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class SessionSuite extends WithMySQLEngine with HiveJDBCTestHelper { + + test("test session") { + withJdbcStatement() { statement => + val resultSet = statement.executeQuery( + "select '1' as id") + val metadata = resultSet.getMetaData + for (i <- 1 to metadata.getColumnCount) { + assert(metadata.getColumnName(i) == "id") + } + while (resultSet.next()) { + val id = resultSet.getObject(1) + assert(id == "1") + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/StatementSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/StatementSuite.scala new file mode 100644 index 00000000000..56ae737fc80 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/StatementSuite.scala @@ -0,0 +1,95 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import java.sql.{Date, Timestamp} + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class StatementSuite extends WithMySQLEngine with HiveJDBCTestHelper { + + test("test select") { + withJdbcStatement("test1") { statement => + statement.execute("create database if not exists db1") + statement.execute("use db1") + statement.execute("create table db1.test1(id bigint, name varchar(255), age int, " + + "PRIMARY KEY ( `id` ))" + + "ENGINE=InnoDB " + + "DEFAULT CHARSET=utf8;") + statement.execute("insert into db1.test1 values(1, 'a', 11)") + + val resultSet1 = statement.executeQuery("select * from db1.test1") + while (resultSet1.next()) { + val id = resultSet1.getObject(1) + assert(id == 1) + val name = resultSet1.getObject(2) + assert(name == "a") + val age = resultSet1.getObject(3) + assert(age == 11) + } + } + } + + test("test types") { + withJdbcStatement("test1") { statement => + statement.execute("create database if not exists db1") + statement.execute("use db1") + statement.execute("create table db1.type_test(" + + "id bigint, " + + "tiny_col tinyint, smallint_col smallint, " + + "int_col int, bigint_col bigint, " + + "decimal_col decimal(27, 9)," + + "date_col date, datetime_col datetime, timestamp_col timestamp," + + "char_col char, varchar_col varchar(255), " + + "boolean_col boolean, " + + "double_col double, float_col float," + + "PRIMARY KEY ( `id` )) " + + "ENGINE=InnoDB " + + "DEFAULT CHARSET=utf8") + statement.execute("insert into db1.type_test" + + "(id, " + + "tiny_col, smallint_col, int_col, bigint_col, " + + "decimal_col, " + + "date_col, datetime_col, timestamp_col," + + "char_col, varchar_col, " + + "boolean_col, " + + "double_col, float_col) " + + "VALUES (1, 2, 3, 4, 5, 6.6, '2023-10-23', '2023-10-23 15:31:45', " + + "'2023-10-23 15:31:45', 'a', 'Hello', true, 7.7, 8.8)") + + val resultSet1 = statement.executeQuery("select * from db1.type_test") + while (resultSet1.next()) { + assert(resultSet1.getObject(1) == 1) + assert(resultSet1.getObject(2) == 2) + assert(resultSet1.getObject(3) == 3) + assert(resultSet1.getObject(4) == 4) + assert(resultSet1.getObject(5) == 5) + assert(resultSet1.getObject(6) == new java.math.BigDecimal("6.600000000")) + assert(resultSet1.getObject(7) == Date.valueOf("2023-10-23")) + assert(resultSet1.getObject(8) == Timestamp.valueOf("2023-10-23 15:31:45")) + assert(resultSet1.getObject(9) == Timestamp.valueOf("2023-10-23 15:31:45")) + assert(resultSet1.getObject(10) == "a") + assert(resultSet1.getObject(11) == "Hello") + assert(resultSet1.getObject(12) == true) + assert(resultSet1.getObject(13) == 7.7) + assert(resultSet1.getObject(14) == 8.8) + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/WithMySQLEngine.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/WithMySQLEngine.scala new file mode 100644 index 00000000000..39d2e0a59ec --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/mysql/WithMySQLEngine.scala @@ -0,0 +1,45 @@ +/* + * 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.kyuubi.engine.jdbc.mysql + +import com.dimafeng.testcontainers.MySQLContainer +import com.dimafeng.testcontainers.scalatest.TestContainerForAll +import org.testcontainers.utility.DockerImageName + +import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.engine.jdbc.WithJdbcEngine + +trait WithMySQLEngine extends WithJdbcEngine with TestContainerForAll { + + private val mysqlDockerImage = "mysql:8.0.32" + + override val containerDef: MySQLContainer.Def = MySQLContainer.Def( + dockerImageName = DockerImageName.parse(mysqlDockerImage), + username = "root", + password = "kyuubi") + + override def withKyuubiConf: Map[String, String] = withContainers { mysqlContainer => + Map( + ENGINE_SHARE_LEVEL.key -> "SERVER", + ENGINE_JDBC_CONNECTION_URL.key -> mysqlContainer.jdbcUrl, + ENGINE_JDBC_CONNECTION_USER.key -> mysqlContainer.username, + ENGINE_JDBC_CONNECTION_PASSWORD.key -> mysqlContainer.password, + ENGINE_TYPE.key -> "jdbc", + ENGINE_JDBC_SHORT_NAME.key -> "mysql", + ENGINE_JDBC_DRIVER_CLASS.key -> "com.mysql.cj.jdbc.Driver") + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/OperationWithPhoenixEngineSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/OperationWithPhoenixEngineSuite.scala index 812efe3ee54..0aacc31549d 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/OperationWithPhoenixEngineSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/OperationWithPhoenixEngineSuite.scala @@ -16,17 +16,16 @@ */ package org.apache.kyuubi.engine.jdbc.phoenix -import org.apache.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} class OperationWithPhoenixEngineSuite extends PhoenixOperationSuite with HiveJDBCTestHelper { override protected def jdbcUrl: String = jdbcConnectionUrl - test("Test for Jdbc engine getInfo") { + test("phoenix - test for Jdbc engine getInfo") { val metaData = ConnectionProvider.create(kyuubiConf).getMetaData withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/SessionSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/SessionSuite.scala index e61d0916e5a..5072741ae27 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/SessionSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/SessionSuite.scala @@ -20,7 +20,7 @@ import org.apache.kyuubi.operation.HiveJDBCTestHelper class SessionSuite extends WithPhoenixEngine with HiveJDBCTestHelper { - test("test session") { + test("phoenix - test session") { withJdbcStatement() { statement => val resultSet = statement.executeQuery( "select '1' as id") diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/StatementSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/StatementSuite.scala index d7e7ebb9b64..21fa1202200 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/StatementSuite.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/StatementSuite.scala @@ -22,7 +22,7 @@ import org.apache.kyuubi.operation.HiveJDBCTestHelper class StatementSuite extends WithPhoenixEngine with HiveJDBCTestHelper { - test("test select") { + test("phoenix - test select") { withJdbcStatement("test1") { statement => statement.execute("create table db1.test1(id bigint primary key, " + "name varchar(255), age integer)") diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/WithPhoenixContainer.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/WithPhoenixContainer.scala index 49b4369bc46..614261e840f 100644 --- a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/WithPhoenixContainer.scala +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/phoenix/WithPhoenixContainer.scala @@ -16,7 +16,7 @@ */ package org.apache.kyuubi.engine.jdbc.phoenix -import com.dimafeng.testcontainers.{GenericContainer, SingleContainer} +import com.dimafeng.testcontainers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait import org.apache.kyuubi.engine.jdbc.WithJdbcServerContainer @@ -27,21 +27,15 @@ trait WithPhoenixContainer extends WithJdbcServerContainer { private val phoenixDockerImage = "iteblog/hbase-phoenix-docker:1.0" - override val container: SingleContainer[_] = GenericContainer( + override val containerDef: GenericContainer.Def[GenericContainer] = GenericContainer.Def( dockerImage = phoenixDockerImage, exposedPorts = Seq(PHOENIX_PORT), waitStrategy = Wait.forListeningPort) - protected def queryServerUrl: String = { + protected def queryServerUrl: String = withContainers { container => val queryServerHost: String = container.host val queryServerPort: Int = container.mappedPort(PHOENIX_PORT) val url = s"$queryServerHost:$queryServerPort" url } - - override def afterAll(): Unit = { - super.afterAll() - container.close() - } - } diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/OperationWithPostgreSQLEngineSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/OperationWithPostgreSQLEngineSuite.scala new file mode 100644 index 00000000000..67e191297aa --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/OperationWithPostgreSQLEngineSuite.scala @@ -0,0 +1,61 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} + +class OperationWithPostgreSQLEngineSuite extends PostgreSQLOperationSuite with HiveJDBCTestHelper { + + override protected def jdbcUrl: String = jdbcConnectionUrl + + test("postgreSQL - test for Jdbc engine getInfo") { + val metaData = ConnectionProvider.create(kyuubiConf).getMetaData + + withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { + withSessionHandle { (client, handle) => + val req = new TGetInfoReq() + req.setSessionHandle(handle) + req.setInfoType(TGetInfoType.CLI_DBMS_NAME) + assert(client.GetInfo(req).getInfoValue.getStringValue == metaData.getDatabaseProductName) + + val req2 = new TGetInfoReq() + req2.setSessionHandle(handle) + req2.setInfoType(TGetInfoType.CLI_DBMS_VER) + assert( + client.GetInfo(req2).getInfoValue.getStringValue == metaData.getDatabaseProductVersion) + + val req3 = new TGetInfoReq() + req3.setSessionHandle(handle) + req3.setInfoType(TGetInfoType.CLI_MAX_COLUMN_NAME_LEN) + assert(client.GetInfo(req3).getInfoValue.getLenValue == metaData.getMaxColumnNameLength) + + val req4 = new TGetInfoReq() + req4.setSessionHandle(handle) + req4.setInfoType(TGetInfoType.CLI_MAX_SCHEMA_NAME_LEN) + assert(client.GetInfo(req4).getInfoValue.getLenValue == metaData.getMaxSchemaNameLength) + + val req5 = new TGetInfoReq() + req5.setSessionHandle(handle) + req5.setInfoType(TGetInfoType.CLI_MAX_TABLE_NAME_LEN) + assert(client.GetInfo(req5).getInfoValue.getLenValue == metaData.getMaxTableNameLength) + } + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLOperationSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLOperationSuite.scala new file mode 100644 index 00000000000..06a76a9a887 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/PostgreSQLOperationSuite.scala @@ -0,0 +1,215 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import java.sql.ResultSet + +import scala.collection.mutable.ArrayBuffer + +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ + +abstract class PostgreSQLOperationSuite extends WithPostgreSQLEngine with HiveJDBCTestHelper { + test("postgreSQL - get catalog") { + case class Catalog(catalog: String) + + withJdbcStatement() { statement => + val meta = statement.getConnection.getMetaData + val resultBuffer = ArrayBuffer[Catalog]() + + val catalogs = meta.getCatalogs + while (catalogs.next()) { + resultBuffer += + Catalog(catalogs.getString("catalog_name")) + } + assert(resultBuffer.contains(Catalog("postgres"))) + resultBuffer.clear() + + } + } + + test("postgreSQL - get schemas") { + case class Schema(catalog: String, schema: String) + + withJdbcStatement() { statement => + val meta = statement.getConnection.getMetaData + val resultBuffer = ArrayBuffer[Schema]() + + val schemas = meta.getSchemas + while (schemas.next()) { + resultBuffer += + Schema(schemas.getString("catalog_name"), schemas.getString("schema_name")) + } + assert(resultBuffer.contains(Schema("postgres", "information_schema"))) + resultBuffer.clear() + } + } + + test("postgreSQL - get tables") { + case class Table(catalog: String, schema: String, tableName: String, tableType: String) + + withJdbcStatement() { statement => + val meta = statement.getConnection.getMetaData + val resultBuffer = ArrayBuffer[Table]() + + var tables = meta.getTables(null, null, null, null) + while (tables.next()) { + resultBuffer += + Table( + null, + null, + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table(null, null, "pg_statistic", "BASE TABLE"))) + assert(resultBuffer.contains(Table(null, null, "pg_roles", "VIEW"))) + resultBuffer.clear() + + statement.execute("create table public.test1(id bigint primary key)") + statement.execute("create table public.test2(id bigint primary key)") + + tables = meta.getTables(null, null, "test1", Array("BASE TABLE")) + while (tables.next()) { + val table = Table( + null, + null, + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + assert(table == Table(null, null, "test1", "BASE TABLE")) + } + + tables = meta.getTables(null, null, "test2", null) + while (tables.next()) { + resultBuffer += Table( + null, + null, + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table(null, null, "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, null, null, Array("BASE TABLE")) + while (tables.next()) { + resultBuffer += Table( + null, + null, + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table(null, null, "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table(null, null, "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, null, null, Array("BASE TABLE", "VIEW")) + while (tables.next()) { + resultBuffer += Table( + null, + null, + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table(null, null, "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table(null, null, "test2", "BASE TABLE"))) + assert(resultBuffer.contains(Table(null, null, "pg_shadow", "VIEW"))) + assert(resultBuffer.contains(Table(null, null, "pg_roles", "VIEW"))) + resultBuffer.clear() + + statement.execute("drop table public.test1") + statement.execute("drop table public.test2") + } + } + + test("postgreSQL - get columns") { + case class Column(tableName: String, columnName: String) + + def buildColumn(resultSet: ResultSet): Column = { + val tableName = resultSet.getString(TABLE_NAME) + val columnName = resultSet.getString(COLUMN_NAME) + val column = Column(tableName, columnName) + column + } + + withJdbcStatement() { statement => + val metadata = statement.getConnection.getMetaData + statement.execute("create table if not exists public.test1" + + "(id bigint primary key, str1 varchar, str2 varchar, age integer)") + + statement.execute("create table if not exists public.test2" + + "(id bigint primary key, str1 varchar, str2 varchar, age integer)") + + val resultBuffer = ArrayBuffer[Column]() + val resultSet1 = metadata.getColumns(null, null, null, null) + while (resultSet1.next()) { + val column = buildColumn(resultSet1) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("test1", "id"))) + assert(resultBuffer.contains(Column("test1", "str1"))) + assert(resultBuffer.contains(Column("test1", "str2"))) + assert(resultBuffer.contains(Column("test1", "age"))) + + assert(resultBuffer.contains(Column("test2", "id"))) + assert(resultBuffer.contains(Column("test2", "str1"))) + assert(resultBuffer.contains(Column("test2", "str2"))) + assert(resultBuffer.contains(Column("test2", "age"))) + + resultBuffer.clear() + + val resultSet2 = metadata.getColumns(null, null, "test1", null) + while (resultSet2.next()) { + val column = buildColumn(resultSet2) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("test1", "id"))) + assert(resultBuffer.contains(Column("test1", "str1"))) + assert(resultBuffer.contains(Column("test1", "str2"))) + assert(resultBuffer.contains(Column("test1", "age"))) + + resultBuffer.clear() + + val resultSet3 = metadata.getColumns(null, null, null, "age") + while (resultSet3.next()) { + val column = buildColumn(resultSet3) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("test1", "age"))) + assert(resultBuffer.contains(Column("test2", "age"))) + + resultBuffer.clear() + + val resultSet4 = metadata.getColumns(null, null, "t%1", "str%") + while (resultSet4.next()) { + val column = buildColumn(resultSet4) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("test1", "str1"))) + + resultBuffer.clear() + + val resultSet5 = metadata.getColumns(null, null, "t%1", "fake") + assert(!resultSet5.next()) + + statement.execute("drop table public.test1") + statement.execute("drop table public.test2") + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/SessionSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/SessionSuite.scala new file mode 100644 index 00000000000..d7fc4dc7b65 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/SessionSuite.scala @@ -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 org.apache.kyuubi.engine.jdbc.postgresql + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class SessionSuite extends WithPostgreSQLEngine with HiveJDBCTestHelper { + + test("postgreSQL - test session") { + withJdbcStatement() { statement => + val resultSet = statement.executeQuery( + "select '1' as id") + val metadata = resultSet.getMetaData + for (i <- 1 to metadata.getColumnCount) { + assert(metadata.getColumnName(i) == "id") + } + while (resultSet.next()) { + val id = resultSet.getObject(1) + assert(id == "1") + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/StatementSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/StatementSuite.scala new file mode 100644 index 00000000000..f2a8ecf6037 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/StatementSuite.scala @@ -0,0 +1,100 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import java.sql.{Date, Timestamp} + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class StatementSuite extends WithPostgreSQLEngine with HiveJDBCTestHelper { + + test("postgreSQL - test select") { + withJdbcStatement("test1") { statement => + statement.execute("create table public.test1(id bigint primary key, " + + "name varchar(255), age integer)") + statement.execute("insert into public.test1 values(1, 'a', 11)") + + val resultSet1 = statement.executeQuery("select * from public.test1") + while (resultSet1.next()) { + val id = resultSet1.getObject(1) + assert(id == 1) + val name = resultSet1.getObject(2) + assert(name == "a") + val age = resultSet1.getObject(3) + assert(age == 11) + } + } + } + + test("postgreSQL - test types") { + withJdbcStatement("type_test") { statement => + statement.execute("create table public.type_test(" + + "id bigint primary key, " + + "smallint_col smallint, " + + "int_col integer, " + + "bigint_col bigint, " + + "date_col date, " + + "timestamp_col timestamp, " + + "char_col char(10), " + + "varchar_col varchar(255), " + + "boolean_col boolean, " + + "double_col double precision, " + + "float_col float)") + statement.execute("insert into public.type_test" + + "(id, " + + "smallint_col, " + + "int_col, " + + "bigint_col, " + + "date_col, " + + "timestamp_col, " + + "char_col, " + + "varchar_col, " + + "boolean_col, " + + "double_col, " + + "float_col) " + + "VALUES (1, " + + "2, " + + "3, " + + "4, " + + "'2022-05-08', " + + "'2022-05-08 17:47:45'," + + "'a', " + + "'Hello', " + + "true, " + + "8.8, " + + "9.9)") + + val resultSet1 = statement.executeQuery("select * from public.type_test") + while (resultSet1.next()) { + val id = resultSet1.getObject(1) + assert(resultSet1.getObject(1) == 1) + assert(resultSet1.getObject(2) == 2) + assert(resultSet1.getObject(3) == 3) + assert(resultSet1.getObject(4) == 4) + assert(resultSet1.getObject(5) == Date.valueOf("2022-05-08")) + assert(resultSet1.getObject(6) == Timestamp.valueOf("2022-05-08 17:47:45")) + assert(resultSet1.getString(7).trim == "a") + assert(resultSet1.getObject(8) == "Hello") + assert(resultSet1.getObject(9) == true) + assert(resultSet1.getObject(10) == 8.8) + assert(resultSet1.getObject(11) == 9.9) + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLContainer.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLContainer.scala new file mode 100644 index 00000000000..32066946ae0 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLContainer.scala @@ -0,0 +1,33 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import com.dimafeng.testcontainers.PostgreSQLContainer +import org.testcontainers.utility.DockerImageName + +import org.apache.kyuubi.engine.jdbc.WithJdbcServerContainer + +trait WithPostgreSQLContainer extends WithJdbcServerContainer { + + private val postgreSQLDockerImage = "postgres:16.1" + + override val containerDef: PostgreSQLContainer.Def = PostgreSQLContainer.Def( + dockerImageName = DockerImageName.parse(postgreSQLDockerImage), + databaseName = "postgres", + username = "kyuubi", + password = "postgres") +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLEngine.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLEngine.scala new file mode 100644 index 00000000000..6d453934e6f --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/postgresql/WithPostgreSQLEngine.scala @@ -0,0 +1,35 @@ +/* + * 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.kyuubi.engine.jdbc.postgresql + +import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.engine.jdbc.WithJdbcEngine + +trait WithPostgreSQLEngine extends WithJdbcEngine with WithPostgreSQLContainer { + + override def withKyuubiConf: Map[String, String] = withContainers { container => + Map( + ENGINE_SHARE_LEVEL.key -> "SERVER", + ENGINE_JDBC_CONNECTION_URL.key -> container.jdbcUrl, + ENGINE_JDBC_CONNECTION_USER.key -> container.username, + ENGINE_JDBC_CONNECTION_PASSWORD.key -> container.password, + ENGINE_TYPE.key -> "jdbc", + ENGINE_JDBC_SHORT_NAME.key -> "postgresql", + ENGINE_JDBC_DRIVER_CLASS.key -> container.driverClassName) + } + +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationSuite.scala new file mode 100644 index 00000000000..575467143ff --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationSuite.scala @@ -0,0 +1,261 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import java.sql.ResultSet + +import scala.collection.mutable.ArrayBuffer + +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ + +abstract class StarRocksOperationSuite extends WithStarRocksEngine with HiveJDBCTestHelper { + test("starrocks - get tables") { + case class Table(catalog: String, schema: String, tableName: String, tableType: String) + + withJdbcStatement() { statement => + val meta = statement.getConnection.getMetaData + val resultBuffer = ArrayBuffer[Table]() + + var tables = meta.getTables(null, null, null, null) + while (tables.next()) { + resultBuffer += + Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "information_schema", "tables", "SYSTEM VIEW"))) + assert(resultBuffer.contains(Table("def", "information_schema", "views", "SYSTEM VIEW"))) + resultBuffer.clear() + + statement.execute("create database if not exists db1") + statement.execute("create table db1.test1(id bigint)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + statement.execute("create table db1.test2(id bigint)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + + statement.execute("create database if not exists db2") + statement.execute("create table db2.test1(id bigint)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + statement.execute("create table db2.test2(id bigint)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + + statement.execute("create view db1.view1 (k1) as select id from db1.test1") + + tables = meta.getTables(null, "db1", "test1", Array("BASE TABLE")) + while (tables.next()) { + val table = Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + assert(table == Table("def", "db1", "test1", "BASE TABLE")) + } + + tables = meta.getTables(null, "db1", null, null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db1", "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, null, "test1", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "db%", "test1", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "db2", "test%", null) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test2", "BASE TABLE"))) + resultBuffer.clear() + + tables = meta.getTables(null, "fake_db", "test1", null) + assert(!tables.next()) + + tables = meta.getTables(null, null, null, Array("VIEW")) + while (tables.next()) { + val table = Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + assert(table == Table("def", "db1", "view1", "VIEW")) + } + + tables = meta.getTables(null, null, null, Array("VIEW", "BASE TABLE")) + while (tables.next()) { + resultBuffer += Table( + tables.getString(TABLE_CATALOG), + tables.getString(TABLE_SCHEMA), + tables.getString(TABLE_NAME), + tables.getString(TABLE_TYPE)) + } + assert(resultBuffer.contains(Table("def", "db1", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db1", "test2", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test1", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db2", "test2", "BASE TABLE"))) + assert(resultBuffer.contains(Table("def", "db1", "view1", "VIEW"))) + resultBuffer.clear() + + statement.execute("drop view db1.view1") + statement.execute("drop table db1.test1") + statement.execute("drop table db1.test2") + statement.execute("drop table db2.test1") + statement.execute("drop table db2.test2") + statement.execute("drop database db1") + statement.execute("drop database db2") + } + } + + test("starrocks - get columns") { + case class Column(tableSchema: String, tableName: String, columnName: String) + + def buildColumn(resultSet: ResultSet): Column = { + val schema = resultSet.getString(TABLE_SCHEMA) + val tableName = resultSet.getString(TABLE_NAME) + val columnName = resultSet.getString(COLUMN_NAME) + val column = Column(schema, tableName, columnName) + column + } + + withJdbcStatement() { statement => + val metadata = statement.getConnection.getMetaData + statement.execute("create database if not exists db1") + statement.execute("create table if not exists db1.test1" + + "(id bigint, str1 string, str2 string, age int)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + statement.execute("create table if not exists db1.test2" + + "(id bigint, str1 string, str2 string, age int)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + + statement.execute("create database if not exists db2") + + statement.execute("create table if not exists db2.test1" + + "(id bigint, str1 string, str2 string, age int)" + + "ENGINE=OLAP DISTRIBUTED BY HASH(`id`) BUCKETS 32 " + + "PROPERTIES ('replication_num' = '1')") + + val resultBuffer = ArrayBuffer[Column]() + val resultSet1 = metadata.getColumns(null, "db1", null, null) + while (resultSet1.next()) { + val column = buildColumn(resultSet1) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "id"))) + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + + assert(resultBuffer.contains(Column("db1", "test2", "id"))) + assert(resultBuffer.contains(Column("db1", "test2", "str1"))) + assert(resultBuffer.contains(Column("db1", "test2", "str2"))) + assert(resultBuffer.contains(Column("db1", "test2", "age"))) + + resultBuffer.clear() + + val resultSet2 = metadata.getColumns(null, null, "test1", null) + while (resultSet2.next()) { + val column = buildColumn(resultSet2) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "id"))) + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + + assert(resultBuffer.contains(Column("db2", "test1", "id"))) + assert(resultBuffer.contains(Column("db2", "test1", "str1"))) + assert(resultBuffer.contains(Column("db2", "test1", "str2"))) + assert(resultBuffer.contains(Column("db2", "test1", "age"))) + + resultBuffer.clear() + + val resultSet3 = metadata.getColumns(null, null, null, "age") + while (resultSet3.next()) { + val column = buildColumn(resultSet3) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "age"))) + assert(resultBuffer.contains(Column("db1", "test2", "age"))) + assert(resultBuffer.contains(Column("db2", "test1", "age"))) + + resultBuffer.clear() + + val resultSet4 = metadata.getColumns(null, "d%1", "t%1", "str%") + while (resultSet4.next()) { + val column = buildColumn(resultSet4) + resultBuffer += column + } + + assert(resultBuffer.contains(Column("db1", "test1", "str1"))) + assert(resultBuffer.contains(Column("db1", "test1", "str2"))) + + resultBuffer.clear() + + val resultSet5 = metadata.getColumns(null, "d%1", "t%1", "fake") + assert(!resultSet5.next()) + + statement.execute("drop table db1.test1") + statement.execute("drop table db1.test2") + statement.execute("drop database db1") + statement.execute("drop table db2.test1") + statement.execute("drop database db2") + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationWithEngineSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationWithEngineSuite.scala new file mode 100644 index 00000000000..acbc028f89d --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksOperationWithEngineSuite.scala @@ -0,0 +1,78 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.engine.jdbc.connection.ConnectionProvider +import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class StarRocksOperationWithEngineSuite extends StarRocksOperationSuite with HiveJDBCTestHelper { + + override protected def jdbcUrl: String = jdbcConnectionUrl + + test("starrocks - test for Jdbc engine getInfo") { + val metaData = ConnectionProvider.create(kyuubiConf).getMetaData + + withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { + withSessionHandle { (client, handle) => + val req = new TGetInfoReq() + req.setSessionHandle(handle) + req.setInfoType(TGetInfoType.CLI_DBMS_NAME) + assert(client.GetInfo(req).getInfoValue.getStringValue == metaData.getDatabaseProductName) + + val req2 = new TGetInfoReq() + req2.setSessionHandle(handle) + req2.setInfoType(TGetInfoType.CLI_DBMS_VER) + assert( + client.GetInfo(req2).getInfoValue.getStringValue == metaData.getDatabaseProductVersion) + + val req3 = new TGetInfoReq() + req3.setSessionHandle(handle) + req3.setInfoType(TGetInfoType.CLI_MAX_COLUMN_NAME_LEN) + assert(client.GetInfo(req3).getInfoValue.getLenValue == metaData.getMaxColumnNameLength) + + val req4 = new TGetInfoReq() + req4.setSessionHandle(handle) + req4.setInfoType(TGetInfoType.CLI_MAX_SCHEMA_NAME_LEN) + assert(client.GetInfo(req4).getInfoValue.getLenValue == metaData.getMaxSchemaNameLength) + + val req5 = new TGetInfoReq() + req5.setSessionHandle(handle) + req5.setInfoType(TGetInfoType.CLI_MAX_TABLE_NAME_LEN) + assert(client.GetInfo(req5).getInfoValue.getLenValue == metaData.getMaxTableNameLength) + } + } + } + + test("starrocks - JDBC ExecuteStatement operation should contain operationLog") { + withSessionHandle { (client, handle) => + val tExecuteStatementReq = new TExecuteStatementReq() + tExecuteStatementReq.setSessionHandle(handle) + tExecuteStatementReq.setStatement("SELECT 1") + val tExecuteStatementResp = client.ExecuteStatement(tExecuteStatementReq) + + val tFetchResultsReq = new TFetchResultsReq() + tFetchResultsReq.setOperationHandle(tExecuteStatementResp.getOperationHandle) + tFetchResultsReq.setFetchType(1) + tFetchResultsReq.setMaxRows(1) + + val tFetchResultsResp = client.FetchResults(tFetchResultsReq) + assert(tFetchResultsResp.getStatus.getStatusCode === TStatusCode.SUCCESS_STATUS) + } + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSessionSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSessionSuite.scala new file mode 100644 index 00000000000..f1c11c96773 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksSessionSuite.scala @@ -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 org.apache.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class StarRocksSessionSuite extends WithStarRocksEngine with HiveJDBCTestHelper { + + test("starrocks - test session") { + withJdbcStatement() { statement => + val resultSet = statement.executeQuery( + "select '1' as id") + val metadata = resultSet.getMetaData + for (i <- 1 to metadata.getColumnCount) { + assert(metadata.getColumnName(i) == "id") + } + while (resultSet.next()) { + val id = resultSet.getObject(1) + assert(id == "1") + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksStatementSuite.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksStatementSuite.scala new file mode 100644 index 00000000000..596701d7e59 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/StarRocksStatementSuite.scala @@ -0,0 +1,105 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import java.sql.{Date, Timestamp} + +import org.apache.kyuubi.operation.HiveJDBCTestHelper + +class StarRocksStatementSuite extends WithStarRocksEngine with HiveJDBCTestHelper { + + test("starrocks - test select") { + withJdbcStatement("test1") { statement => + statement.execute("create database if not exists db1") + statement.execute("use db1") + statement.execute( + """CREATE TABLE db1.test1(id bigint, name varchar(255), age int) + | ENGINE=OLAP + | DISTRIBUTED BY HASH(`id`) + | PROPERTIES ('replication_num' = '1', 'in_memory' = 'true') + |""".stripMargin) + statement.execute("insert into db1.test1 values(1, 'a', 11)") + + val resultSet1 = statement.executeQuery("select * from db1.test1") + while (resultSet1.next()) { + val id = resultSet1.getObject(1) + assert(id == 1) + val name = resultSet1.getObject(2) + assert(name == "a") + val age = resultSet1.getObject(3) + assert(age == 11) + } + } + } + + test("starrocks - test types") { + withJdbcStatement("test1") { statement => + statement.execute("create database if not exists db1") + statement.execute("use db1") + statement.execute( + """ CREATE TABLE db1.type_test( + | id bigint, + | tiny_col tinyint, + | smallint_col smallint, + | int_col int, + | bigint_col bigint, + | largeint_col largeint, + | decimal_col decimal(27, 9), + | date_col date, + | datetime_col datetime, + | char_col char, + | varchar_col varchar(255), + | string_col string, + | boolean_col boolean, + | double_col double, + | float_col float) + | ENGINE=OLAP + | DISTRIBUTED BY HASH(`id`) + | PROPERTIES ('replication_num' = '1', 'in_memory' = 'true') + |""".stripMargin) + statement.execute( + """ insert into db1.type_test + | (id, tiny_col, smallint_col, int_col, bigint_col, largeint_col, decimal_col, + | date_col, datetime_col, char_col, varchar_col, string_col, boolean_col, + | double_col, float_col) + | VALUES (1, 2, 3, 4, 5, 6, 7.7, + | '2022-05-08', '2022-05-08 17:47:45', 'a', 'Hello', 'Hello, Kyuubi', true, + | 8.8, 9.9) + |""".stripMargin) + val resultSet1 = statement.executeQuery("select * from db1.type_test") + while (resultSet1.next()) { + assert(resultSet1.getObject(1) == 1) + assert(resultSet1.getObject(2) == 2) + assert(resultSet1.getObject(3) == 3) + assert(resultSet1.getObject(4) == 4) + assert(resultSet1.getObject(5) == 5) + assert(resultSet1.getObject(6) == "6") + assert(resultSet1.getObject(7) == new java.math.BigDecimal("7.700000000")) + assert(resultSet1.getObject(8) == Date.valueOf("2022-05-08")) + assert(resultSet1.getObject(9) == Timestamp.valueOf("2022-05-08 17:47:45")) + assert(resultSet1.getObject(10) == "a") + assert(resultSet1.getObject(11) == "Hello") + assert(resultSet1.getObject(12) == "Hello, Kyuubi") + assert(resultSet1.getObject(13) == true) + assert(resultSet1.getObject(14) == 8.8) + assert(resultSet1.getObject(15) == 9.9) + } + } + } + + override protected def jdbcUrl: String = jdbcConnectionUrl +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksContainer.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksContainer.scala new file mode 100644 index 00000000000..9c229a636cb --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksContainer.scala @@ -0,0 +1,57 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import java.time.Duration + +import com.dimafeng.testcontainers.GenericContainer +import org.testcontainers.containers.wait.strategy.{Wait, WaitAllStrategy} +import org.testcontainers.containers.wait.strategy.Wait._ + +import org.apache.kyuubi.engine.jdbc.WithJdbcServerContainer + +trait WithStarRocksContainer extends WithJdbcServerContainer { + + private val starrocksDockerImage = "starrocks/allin1-ubuntu:3.1.6" + + private val STARROCKS_FE_MYSQL_PORT = 9030 + private val STARROCKS_FE_HTTP_PORT = 8030 + private val STARROCKS_BE_THRIFT_PORT = 9060 + private val STARROCKS_BE_HTTP_PORT = 8040 + private val STARROCKS_BE_HEARTBEAT_PORT = 9050 + private val ports = Seq( + STARROCKS_FE_MYSQL_PORT, + STARROCKS_FE_HTTP_PORT, + STARROCKS_BE_THRIFT_PORT, + STARROCKS_BE_HTTP_PORT, + STARROCKS_BE_HEARTBEAT_PORT) + + override val containerDef: GenericContainer.Def[GenericContainer] = GenericContainer.Def( + dockerImage = starrocksDockerImage, + exposedPorts = ports, + waitStrategy = new WaitAllStrategy().withStartupTimeout(Duration.ofMinutes(10)) + .withStrategy(Wait.forListeningPorts(ports: _*)) + .withStrategy(forLogMessage(".*broker service already added into FE service.*", 1)) + .withStrategy( + forLogMessage(".*Enjoy the journal to StarRocks blazing-fast lake-house engine.*", 1))) + + protected def feJdbcUrl: String = withContainers { container => + val queryServerHost: String = container.host + val queryServerPort: Int = container.mappedPort(STARROCKS_FE_MYSQL_PORT) + s"jdbc:mysql://$queryServerHost:$queryServerPort" + } +} diff --git a/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksEngine.scala b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksEngine.scala new file mode 100644 index 00000000000..6423186c050 --- /dev/null +++ b/externals/kyuubi-jdbc-engine/src/test/scala/org/apache/kyuubi/engine/jdbc/starrocks/WithStarRocksEngine.scala @@ -0,0 +1,36 @@ +/* + * 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.kyuubi.engine.jdbc.starrocks + +import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.engine.jdbc.WithJdbcEngine +import org.apache.kyuubi.engine.jdbc.mysql.MySQL8ConnectionProvider + +trait WithStarRocksEngine extends WithJdbcEngine with WithStarRocksContainer { + + private val user = "root" + private val password = "" + + override def withKyuubiConf: Map[String, String] = Map( + ENGINE_SHARE_LEVEL.key -> "SERVER", + ENGINE_JDBC_CONNECTION_URL.key -> feJdbcUrl, + ENGINE_JDBC_CONNECTION_USER.key -> user, + ENGINE_JDBC_CONNECTION_PASSWORD.key -> password, + ENGINE_TYPE.key -> "jdbc", + ENGINE_JDBC_SHORT_NAME.key -> "starrocks", + ENGINE_JDBC_DRIVER_CLASS.key -> MySQL8ConnectionProvider.driverClass) +} diff --git a/externals/kyuubi-spark-sql-engine/pom.xml b/externals/kyuubi-spark-sql-engine/pom.xml index 555e41a44b6..4317b2ede37 100644 --- a/externals/kyuubi-spark-sql-engine/pom.xml +++ b/externals/kyuubi-spark-sql-engine/pom.xml @@ -238,9 +238,7 @@ io.perfmark:perfmark-api io.vertx:* net.jodah:failsafe - org.apache.hive:hive-service-rpc org.apache.kyuubi:* - org.apache.thrift:* org.checkerframework:checker-qual org.codehaus.mojo:animal-sniffer-annotations @@ -265,27 +263,6 @@ - - org.apache.hive.service.rpc.thrift - ${kyuubi.shade.packageName}.org.apache.hive.service.rpc.thrift - - org.apache.hive.service.rpc.thrift.** - - - - com.facebook.fb303 - ${kyuubi.shade.packageName}.com.facebook.fb303 - - com.facebook.fb303.** - - - - org.apache.thrift - ${kyuubi.shade.packageName}.org.apache.thrift - - org.apache.thrift.** - - io.etcd ${kyuubi.shade.packageName}.io.etcd diff --git a/externals/kyuubi-spark-sql-engine/src/main/resources/python/execute_python.py b/externals/kyuubi-spark-sql-engine/src/main/resources/python/execute_python.py index e6fe7f92bcf..6729092f75d 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/resources/python/execute_python.py +++ b/externals/kyuubi-spark-sql-engine/src/main/resources/python/execute_python.py @@ -16,6 +16,8 @@ # import ast +import datetime +import decimal import io import json @@ -23,6 +25,7 @@ import re import sys import traceback +import base64 from glob import glob if sys.version_info[0] < 3: @@ -70,6 +73,8 @@ global_dict = {} +MAGIC_ENABLED = os.environ.get("MAGIC_ENABLED") == "true" + class NormalNode(object): def __init__(self, code): @@ -94,6 +99,36 @@ def execute(self): raise ExecutionError(sys.exc_info()) +class UnknownMagic(Exception): + pass + + +class MagicNode(object): + def __init__(self, line): + parts = line[1:].split(" ", 1) + if len(parts) == 1: + self.magic, self.rest = parts[0], () + else: + self.magic, self.rest = parts[0], (parts[1],) + + def execute(self): + if not self.magic: + raise UnknownMagic("magic command not specified") + + try: + handler = magic_router[self.magic] + except KeyError: + raise UnknownMagic("unknown magic command '%s'" % self.magic) + + try: + return handler(*self.rest) + except ExecutionError as e: + raise e + except Exception: + exc_type, exc_value, tb = sys.exc_info() + raise ExecutionError((exc_type, exc_value, None)) + + class ExecutionError(Exception): def __init__(self, exc_info): self.exc_info = exc_info @@ -118,6 +153,14 @@ def parse_code_into_nodes(code): try: nodes.append(NormalNode(code)) except SyntaxError: + # It's possible we hit a syntax error because of a magic command. Split the code groups + # of 'normal code', and code that starts with a '%'. possibly magic code lines, and see + # if any of the lines. Remove lines until we find a node that parses, then check if the + # next line is a magic line. + + # Split the code into chunks of normal code, and possibly magic code, which starts with + # a '%'. + normal = [] chunks = [] for i, line in enumerate(code.rstrip().split("\n")): @@ -135,24 +178,22 @@ def parse_code_into_nodes(code): # Convert the chunks into AST nodes. Let exceptions propagate. for chunk in chunks: - # TODO: look back here when Jupyter and sparkmagic are supported - # if chunk.startswith('%'): - # nodes.append(MagicNode(chunk)) - - nodes.append(NormalNode(chunk)) + if MAGIC_ENABLED and chunk.startswith("%"): + nodes.append(MagicNode(chunk)) + else: + nodes.append(NormalNode(chunk)) return nodes def execute_reply(status, content): - msg = { + return { "msg_type": "execute_reply", "content": dict( content, status=status, ), } - return json.dumps(msg) def execute_reply_ok(data): @@ -211,6 +252,9 @@ def execute_request(content): try: for node in nodes: result = node.execute() + except UnknownMagic: + exc_type, exc_value, tb = sys.exc_info() + return execute_reply_error(exc_type, exc_value, None) except ExecutionError as e: return execute_reply_error(*e.exc_info) @@ -239,6 +283,171 @@ def execute_request(content): return execute_reply_ok(result) +def magic_table_convert(value): + try: + converter = magic_table_types[type(value)] + except KeyError: + converter = magic_table_types[str] + + return converter(value) + + +def magic_table_convert_seq(items): + last_item_type = None + converted_items = [] + + for item in items: + item_type, item = magic_table_convert(item) + + if last_item_type is None: + last_item_type = item_type + elif last_item_type != item_type: + raise ValueError("value has inconsistent types") + + converted_items.append(item) + + return "ARRAY_TYPE", converted_items + + +def magic_table_convert_map(m): + last_key_type = None + last_value_type = None + converted_items = {} + + for key, value in m: + key_type, key = magic_table_convert(key) + value_type, value = magic_table_convert(value) + + if last_key_type is None: + last_key_type = key_type + elif last_value_type != value_type: + raise ValueError("value has inconsistent types") + + if last_value_type is None: + last_value_type = value_type + elif last_value_type != value_type: + raise ValueError("value has inconsistent types") + + converted_items[key] = value + + return "MAP_TYPE", converted_items + + +magic_table_types = { + type(None): lambda x: ("NULL_TYPE", x), + bool: lambda x: ("BOOLEAN_TYPE", x), + int: lambda x: ("INT_TYPE", x), + float: lambda x: ("DOUBLE_TYPE", x), + str: lambda x: ("STRING_TYPE", str(x)), + datetime.date: lambda x: ("DATE_TYPE", str(x)), + datetime.datetime: lambda x: ("TIMESTAMP_TYPE", str(x)), + decimal.Decimal: lambda x: ("DECIMAL_TYPE", str(x)), + tuple: magic_table_convert_seq, + list: magic_table_convert_seq, + dict: magic_table_convert_map, +} + + +def magic_table(name): + try: + value = global_dict[name] + except KeyError: + exc_type, exc_value, tb = sys.exc_info() + raise ExecutionError((exc_type, exc_value, None)) + + if not isinstance(value, (list, tuple)): + value = [value] + + headers = {} + data = [] + + for row in value: + cols = [] + data.append(cols) + + if "Row" == row.__class__.__name__: + row = row.asDict() + + if not isinstance(row, (list, tuple, dict)): + row = [row] + + if isinstance(row, (list, tuple)): + iterator = enumerate(row) + else: + iterator = sorted(row.items()) + + for name, col in iterator: + col_type, col = magic_table_convert(col) + + try: + header = headers[name] + except KeyError: + header = { + "name": str(name), + "type": col_type, + } + headers[name] = header + else: + # Reject columns that have a different type. (allow none value) + if col_type != "NULL_TYPE" and header["type"] != col_type: + if header["type"] == "NULL_TYPE": + header["type"] = col_type + else: + exc_type = Exception + exc_value = Exception("table rows have different types") + raise ExecutionError((exc_type, exc_value, None)) + + cols.append(col) + + headers = [v for k, v in sorted(headers.items())] + + return { + "application/vnd.livy.table.v1+json": { + "headers": headers, + "data": data, + } + } + + +def magic_json(name): + try: + value = global_dict[name] + except KeyError: + exc_type, exc_value, tb = sys.exc_info() + raise ExecutionError((exc_type, exc_value, None)) + + return { + "application/json": value, + } + + +def magic_matplot(name): + try: + value = global_dict[name] + fig = value.gcf() + imgdata = io.BytesIO() + fig.savefig(imgdata, format="png") + imgdata.seek(0) + encode = base64.b64encode(imgdata.getvalue()) + if sys.version >= "3": + encode = encode.decode() + + except: + exc_type, exc_value, tb = sys.exc_info() + raise ExecutionError((exc_type, exc_value, None)) + + return { + "image/png": encode, + } + + +magic_router = { + "table": magic_table, + "json": magic_json, + "matplot": magic_matplot, +} + + # get or create spark session spark_session = kyuubi_util.get_spark_session( os.environ.get("KYUUBI_SPARK_SESSION_UUID") @@ -278,6 +487,22 @@ def main(): break result = execute_request(content) + + try: + result = json.dumps(result) + except ValueError: + result = json.dumps( + { + "msg_type": "inspect_reply", + "content": { + "status": "error", + "ename": "ValueError", + "evalue": "cannot json-ify %s" % response, + "traceback": [], + }, + } + ) + print(result, file=sys_stdout) sys_stdout.flush() clearOutputs() diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/KyuubiSparkUtil.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/KyuubiSparkUtil.scala index b9fb9325999..2e33d8ce6db 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/KyuubiSparkUtil.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/KyuubiSparkUtil.scala @@ -26,6 +26,7 @@ import org.apache.spark.sql.SparkSession import org.apache.spark.util.kvstore.KVIndex import org.apache.kyuubi.Logging +import org.apache.kyuubi.config.ConfigEntry import org.apache.kyuubi.util.SemanticVersion object KyuubiSparkUtil extends Logging { @@ -98,4 +99,18 @@ object KyuubiSparkUtil extends Logging { // Given that we are on the Spark SQL engine side, the [[org.apache.spark.SPARK_VERSION]] can be // represented as the runtime version of the Spark SQL engine. lazy val SPARK_ENGINE_RUNTIME_VERSION: SemanticVersion = SemanticVersion(SPARK_VERSION) + + /** + * Get session level config value + * @param configEntry configEntry + * @param spark sparkSession + * @tparam T any type + * @return session level config value, if spark not set this config, + * default return kyuubi's config + */ + def getSessionConf[T](configEntry: ConfigEntry[T], spark: SparkSession): T = { + spark.conf.getOption(configEntry.key).map(configEntry.valueConverter).getOrElse { + SparkSQLEngine.kyuubiConf.get(configEntry) + } + } } diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkSQLEngine.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkSQLEngine.scala index ba84e1b1b3a..3dc771e6ccf 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkSQLEngine.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkSQLEngine.scala @@ -26,6 +26,7 @@ import scala.concurrent.duration.Duration import scala.util.control.NonFatal import com.google.common.annotations.VisibleForTesting +import org.apache.hadoop.fs.Path import org.apache.spark.{ui, SparkConf} import org.apache.spark.kyuubi.{SparkContextHelper, SparkSQLEngineEventListener, SparkSQLEngineListener} import org.apache.spark.kyuubi.SparkUtilsHelper.getLocalDir @@ -37,6 +38,7 @@ import org.apache.kyuubi.config.{KyuubiConf, KyuubiReservedKeys} import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_ENGINE_SUBMIT_TIME_KEY, KYUUBI_ENGINE_URL} import org.apache.kyuubi.engine.ShareLevel +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.engineId import org.apache.kyuubi.engine.spark.SparkSQLEngine.{countDownLatch, currentEngine} import org.apache.kyuubi.engine.spark.events.{EngineEvent, EngineEventsStore, SparkEventHandlerRegister} import org.apache.kyuubi.engine.spark.session.SparkSessionImpl @@ -46,6 +48,7 @@ import org.apache.kyuubi.ha.client.RetryPolicies import org.apache.kyuubi.service.Serverable import org.apache.kyuubi.session.SessionHandle import org.apache.kyuubi.util.{SignalRegister, ThreadUtils} +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay case class SparkSQLEngine(spark: SparkSession) extends Serverable("SparkSQLEngine") { @@ -57,6 +60,7 @@ case class SparkSQLEngine(spark: SparkSession) extends Serverable("SparkSQLEngin @volatile private var lifetimeTerminatingChecker: Option[ScheduledExecutorService] = None @volatile private var stopEngineExec: Option[ThreadPoolExecutor] = None + @volatile private var engineSavePath: Option[String] = None override def initialize(conf: KyuubiConf): Unit = { val listener = new SparkSQLEngineListener(this) @@ -86,6 +90,15 @@ case class SparkSQLEngine(spark: SparkSession) extends Serverable("SparkSQLEngin maxInitTimeout > 0) { startFastFailChecker(maxInitTimeout) } + + if (backendService.sessionManager.getConf.get(OPERATION_RESULT_SAVE_TO_FILE)) { + val savePath = backendService.sessionManager.getConf.get(OPERATION_RESULT_SAVE_TO_FILE_DIR) + engineSavePath = Some(s"$savePath/$engineId") + val path = new Path(engineSavePath.get) + val fs = path.getFileSystem(spark.sparkContext.hadoopConfiguration) + fs.mkdirs(path) + fs.deleteOnExit(path) + } } override def stop(): Unit = if (shutdown.compareAndSet(false, true)) { @@ -101,6 +114,10 @@ case class SparkSQLEngine(spark: SparkSession) extends Serverable("SparkSQLEngin exec, Duration(60, TimeUnit.SECONDS)) }) + engineSavePath.foreach { p => + val path = new Path(p) + path.getFileSystem(spark.sparkContext.hadoopConfiguration).delete(path, true) + } } def gracefulStop(): Unit = if (gracefulStopDeregistered.compareAndSet(false, true)) { @@ -167,7 +184,8 @@ case class SparkSQLEngine(spark: SparkSession) extends Serverable("SparkSQLEngin } lifetimeTerminatingChecker = Some(ThreadUtils.newDaemonSingleThreadScheduledExecutor("spark-engine-lifetime-checker")) - lifetimeTerminatingChecker.get.scheduleWithFixedDelay( + scheduleTolerableRunnableWithFixedDelay( + lifetimeTerminatingChecker.get, checkTask, interval, interval, @@ -288,7 +306,8 @@ object SparkSQLEngine extends Logging { KyuubiSparkUtil.initializeSparkSession( session, - kyuubiConf.get(ENGINE_INITIALIZE_SQL) ++ kyuubiConf.get(ENGINE_SESSION_INITIALIZE_SQL)) + kyuubiConf.get(ENGINE_SPARK_INITIALIZE_SQL) ++ kyuubiConf.get( + ENGINE_SESSION_SPARK_INITIALIZE_SQL)) session.sparkContext.setLocalProperty(KYUUBI_ENGINE_URL, KyuubiSparkUtil.engineUrl) session } @@ -361,7 +380,8 @@ object SparkSQLEngine extends Logging { // blocking main thread countDownLatch.await() } catch { - case e: KyuubiException => currentEngine match { + case e: KyuubiException => + currentEngine match { case Some(engine) => engine.stop() val event = EngineEvent(engine) @@ -370,16 +390,21 @@ object SparkSQLEngine extends Logging { error(event, e) case _ => error("Current SparkSQLEngine is not created.") } + throw e } } catch { case i: InterruptedException if !sparkSessionCreated.get => - error( + val msg = s"The Engine main thread was interrupted, possibly due to `createSpark` timeout." + s" The `${ENGINE_INIT_TIMEOUT.key}` is ($initTimeout ms) " + - s" and submitted at $submitTime.", - i) - case t: Throwable => error(s"Failed to instantiate SparkSession: ${t.getMessage}", t) + s" and submitted at $submitTime." + error(msg, i) + throw new InterruptedException(msg) + case e: KyuubiException => throw e + case t: Throwable => + error(s"Failed to instantiate SparkSession: ${t.getMessage}", t) + throw t } finally { if (spark != null) { spark.stop() diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkTBinaryFrontendService.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkTBinaryFrontendService.scala index c2563b32bce..7ca2e8fbed2 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkTBinaryFrontendService.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/SparkTBinaryFrontendService.scala @@ -23,7 +23,6 @@ import org.apache.hadoop.conf.Configuration import org.apache.hadoop.io.Text import org.apache.hadoop.security.{Credentials, UserGroupInformation} import org.apache.hadoop.security.token.{Token, TokenIdentifier} -import org.apache.hive.service.rpc.thrift.{TOpenSessionReq, TOpenSessionResp, TRenewDelegationTokenReq, TRenewDelegationTokenResp} import org.apache.spark.SparkContext import org.apache.spark.kyuubi.SparkContextHelper @@ -33,6 +32,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.ha.client.{EngineServiceDiscovery, ServiceDiscovery} import org.apache.kyuubi.service.{Serverable, Service, TBinaryFrontendService} import org.apache.kyuubi.service.TFrontendService._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TOpenSessionReq, TOpenSessionResp, TRenewDelegationTokenReq, TRenewDelegationTokenResp} import org.apache.kyuubi.util.KyuubiHadoopUtils import org.apache.kyuubi.util.reflect.DynConstructors diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecutePython.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecutePython.scala index badd835301a..f60b1d4c899 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecutePython.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecutePython.scala @@ -28,8 +28,6 @@ import javax.ws.rs.core.UriBuilder import scala.collection.JavaConverters._ -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.commons.lang3.StringUtils import org.apache.spark.SparkFiles import org.apache.spark.api.python.KyuubiPythonGatewayServer @@ -37,9 +35,11 @@ import org.apache.spark.sql.{Row, SparkSession} import org.apache.spark.sql.types.StructType import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils} -import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SPARK_PYTHON_ENV_ARCHIVE, ENGINE_SPARK_PYTHON_ENV_ARCHIVE_EXEC_PATH, ENGINE_SPARK_PYTHON_HOME_ARCHIVE} +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SPARK_PYTHON_ENV_ARCHIVE, ENGINE_SPARK_PYTHON_ENV_ARCHIVE_EXEC_PATH, ENGINE_SPARK_PYTHON_HOME_ARCHIVE, ENGINE_SPARK_PYTHON_MAGIC_ENABLED} +import org.apache.kyuubi.config.KyuubiConf.EngineSparkOutputMode.{AUTO, EngineSparkOutputMode, NOTEBOOK} import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_SESSION_USER_KEY, KYUUBI_STATEMENT_ID_KEY} import org.apache.kyuubi.engine.spark.KyuubiSparkUtil._ +import org.apache.kyuubi.engine.spark.util.JsonUtils import org.apache.kyuubi.operation.{ArrayFetchIterator, OperationHandle, OperationState} import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session @@ -87,7 +87,7 @@ class ExecutePython( val response = worker.runCode(statement) val status = response.map(_.content.status).getOrElse("UNKNOWN_STATUS") if (PythonResponse.OK_STATUS.equalsIgnoreCase(status)) { - val output = response.map(_.content.getOutput()).getOrElse("") + val output = response.map(_.content.getOutput(outputMode)).getOrElse("") val ename = response.map(_.content.getEname()).getOrElse("") val evalue = response.map(_.content.getEvalue()).getOrElse("") val traceback = response.map(_.content.getTraceback()).getOrElse(Seq.empty) @@ -95,7 +95,8 @@ class ExecutePython( new ArrayFetchIterator[Row](Array(Row(output, status, ename, evalue, traceback))) setState(OperationState.FINISHED) } else { - throw KyuubiSQLException(s"Interpret error:\n$statement\n $response") + throw KyuubiSQLException(s"Interpret error:\n" + + s"${JsonUtils.toPrettyJson(Map("code" -> statement, "response" -> response.orNull))}") } } } catch { @@ -200,12 +201,12 @@ case class SessionPythonWorker( throw KyuubiSQLException("Python worker process has been exited, please check the error log" + " and re-create the session to run python code.") } - val input = ExecutePython.toJson(Map("code" -> code, "cmd" -> "run_code")) + val input = JsonUtils.toJson(Map("code" -> code, "cmd" -> "run_code")) // scalastyle:off println stdin.println(input) // scalastyle:on stdin.flush() - val pythonResponse = Option(stdout.readLine()).map(ExecutePython.fromJson[PythonResponse](_)) + val pythonResponse = Option(stdout.readLine()).map(JsonUtils.fromJson[PythonResponse](_)) // throw exception if internal python code fail if (internal && !pythonResponse.map(_.content.status).contains(PythonResponse.OK_STATUS)) { throw KyuubiSQLException(s"Internal python code $code failure: $pythonResponse") @@ -214,7 +215,7 @@ case class SessionPythonWorker( } def close(): Unit = { - val exitCmd = ExecutePython.toJson(Map("cmd" -> "exit_worker")) + val exitCmd = JsonUtils.toJson(Map("cmd" -> "exit_worker")) // scalastyle:off println stdin.println(exitCmd) // scalastyle:on @@ -233,6 +234,7 @@ object ExecutePython extends Logging { final val PY4J_REGEX = "py4j-[\\S]*.zip$".r final val PY4J_PATH = "PY4J_PATH" final val IS_PYTHON_APP_KEY = "spark.yarn.isPython" + final val MAGIC_ENABLED = "MAGIC_ENABLED" private val isPythonGatewayStart = new AtomicBoolean(false) private val kyuubiPythonPath = Utils.createTempDir() @@ -280,6 +282,7 @@ object ExecutePython extends Logging { } env.put("KYUUBI_SPARK_SESSION_UUID", sessionId) env.put("PYTHON_GATEWAY_CONNECTION_INFO", KyuubiPythonGatewayServer.CONNECTION_FILE_PATH) + env.put(MAGIC_ENABLED, getSessionConf(ENGINE_SPARK_PYTHON_MAGIC_ENABLED, spark).toString) logger.info( s""" |launch python worker command: ${builder.command().asScala.mkString(" ")} @@ -295,10 +298,8 @@ object ExecutePython extends Logging { } def getSparkPythonExecFromArchive(spark: SparkSession, session: Session): Option[String] = { - val pythonEnvArchive = spark.conf.getOption(ENGINE_SPARK_PYTHON_ENV_ARCHIVE.key) - .orElse(session.sessionManager.getConf.get(ENGINE_SPARK_PYTHON_ENV_ARCHIVE)) - val pythonEnvExecPath = spark.conf.getOption(ENGINE_SPARK_PYTHON_ENV_ARCHIVE_EXEC_PATH.key) - .getOrElse(session.sessionManager.getConf.get(ENGINE_SPARK_PYTHON_ENV_ARCHIVE_EXEC_PATH)) + val pythonEnvArchive = getSessionConf(ENGINE_SPARK_PYTHON_ENV_ARCHIVE, spark) + val pythonEnvExecPath = getSessionConf(ENGINE_SPARK_PYTHON_ENV_ARCHIVE_EXEC_PATH, spark) pythonEnvArchive.map { archive => var uri = new URI(archive) @@ -311,8 +312,7 @@ object ExecutePython extends Logging { } def getSparkPythonHomeFromArchive(spark: SparkSession, session: Session): Option[String] = { - val pythonHomeArchive = spark.conf.getOption(ENGINE_SPARK_PYTHON_HOME_ARCHIVE.key) - .orElse(session.sessionManager.getConf.get(ENGINE_SPARK_PYTHON_HOME_ARCHIVE)) + val pythonHomeArchive = getSessionConf(ENGINE_SPARK_PYTHON_HOME_ARCHIVE, spark) pythonHomeArchive.map { archive => var uri = new URI(archive) @@ -388,19 +388,6 @@ object ExecutePython extends Logging { sink.close() file } - - val mapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) - def toJson[T](obj: T): String = { - mapper.writeValueAsString(obj) - } - def fromJson[T](json: String, clz: Class[T]): T = { - mapper.readValue(json, clz) - } - - def fromJson[T](json: String)(implicit m: Manifest[T]): T = { - mapper.readValue(json, m.runtimeClass).asInstanceOf[T] - } - } case class PythonResponse( @@ -412,15 +399,28 @@ object PythonResponse { } case class PythonResponseContent( - data: Map[String, String], + data: Map[String, Object], ename: String, evalue: String, traceback: Seq[String], status: String) { - def getOutput(): String = { - Option(data) - .map(_.getOrElse("text/plain", "")) - .getOrElse("") + def getOutput(outputMode: EngineSparkOutputMode): String = { + if (data == null) return "" + + outputMode match { + case AUTO => + // If data does not contains field other than `test/plain`, keep backward compatibility, + // otherwise, return all the data. + if (data.filterNot(_._1 == "text/plain").isEmpty) { + data.get("text/plain").map { + case str: String => str + case obj => JsonUtils.toJson(obj) + }.getOrElse("") + } else { + JsonUtils.toJson(data) + } + case NOTEBOOK => JsonUtils.toJson(data) + } } def getEname(): String = { Option(ename).getOrElse("") diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteScala.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteScala.scala index 691c4fb32d3..092e6e8241c 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteScala.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteScala.scala @@ -31,6 +31,7 @@ import org.apache.spark.sql.types.StructType import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.engine.spark.KyuubiSparkUtil._ import org.apache.kyuubi.engine.spark.repl.KyuubiSparkILoop +import org.apache.kyuubi.engine.spark.util.JsonUtils import org.apache.kyuubi.operation.{ArrayFetchIterator, OperationHandle, OperationState} import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session @@ -119,7 +120,8 @@ class ExecuteScala( } } case Error => - throw KyuubiSQLException(s"Interpret error:\n$statement\n ${repl.getOutput}") + throw KyuubiSQLException(s"Interpret error:\n" + + s"${JsonUtils.toPrettyJson(Map("code" -> statement, "response" -> repl.getOutput))}") case Incomplete => throw KyuubiSQLException(s"Incomplete code:\n$statement") } diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteStatement.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteStatement.scala index 17d8a741269..8b47e2075a0 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteStatement.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/ExecuteStatement.scala @@ -19,14 +19,16 @@ package org.apache.kyuubi.engine.spark.operation import java.util.concurrent.RejectedExecutionException +import scala.Array._ import scala.collection.JavaConverters._ +import org.apache.hadoop.fs.Path import org.apache.spark.sql.DataFrame import org.apache.spark.sql.kyuubi.SparkDatasetHelper._ import org.apache.spark.sql.types._ import org.apache.kyuubi.{KyuubiSQLException, Logging} -import org.apache.kyuubi.config.KyuubiConf.OPERATION_RESULT_MAX_ROWS +import org.apache.kyuubi.config.KyuubiConf.{OPERATION_RESULT_MAX_ROWS, OPERATION_RESULT_SAVE_TO_FILE, OPERATION_RESULT_SAVE_TO_FILE_DIR, OPERATION_RESULT_SAVE_TO_FILE_MINSIZE} import org.apache.kyuubi.engine.spark.KyuubiSparkUtil._ import org.apache.kyuubi.engine.spark.session.SparkSessionImpl import org.apache.kyuubi.operation.{ArrayFetchIterator, FetchIterator, IterableFetchIterator, OperationHandle, OperationState} @@ -46,6 +48,8 @@ class ExecuteStatement( override def getOperationLog: Option[OperationLog] = Option(operationLog) override protected def supportProgress: Boolean = true + private var fetchOrcStatement: Option[FetchOrcStatement] = None + private var saveFileName: Option[String] = None override protected def resultSchema: StructType = { if (result == null || result.schema.isEmpty) { new StructType().add("Result", "string") @@ -64,6 +68,15 @@ class ExecuteStatement( OperationLog.removeCurrentOperationLog() } + override def close(): Unit = { + super.close() + fetchOrcStatement.foreach(_.close()) + saveFileName.foreach { p => + val path = new Path(p) + path.getFileSystem(spark.sparkContext.hadoopConfiguration).delete(path, true) + } + } + protected def incrementalCollectResult(resultDF: DataFrame): Iterator[Any] = { resultDF.toLocalIterator().asScala } @@ -148,8 +161,7 @@ class ExecuteStatement( s"__kyuubi_operation_result_arrow_timestampAsString__=$timestampAsString") private def collectAsIterator(resultDF: DataFrame): FetchIterator[_] = { - val resultMaxRows = spark.conf.getOption(OPERATION_RESULT_MAX_ROWS.key).map(_.toInt) - .getOrElse(session.sessionManager.getConf.get(OPERATION_RESULT_MAX_ROWS)) + val resultMaxRows: Int = getSessionConf(OPERATION_RESULT_MAX_ROWS, spark) if (incrementalCollect) { if (resultMaxRows > 0) { warn(s"Ignore ${OPERATION_RESULT_MAX_ROWS.key} on incremental collect mode.") @@ -159,6 +171,31 @@ class ExecuteStatement( override def iterator: Iterator[Any] = incrementalCollectResult(resultDF) }) } else { + val resultSaveEnabled = getSessionConf(OPERATION_RESULT_SAVE_TO_FILE, spark) + lazy val resultSaveThreshold = getSessionConf(OPERATION_RESULT_SAVE_TO_FILE_MINSIZE, spark) + if (hasResultSet && resultSaveEnabled && shouldSaveResultToFs( + resultMaxRows, + resultSaveThreshold, + result)) { + val sessionId = session.handle.identifier.toString + val savePath = session.sessionManager.getConf.get(OPERATION_RESULT_SAVE_TO_FILE_DIR) + saveFileName = Some(s"$savePath/$engineId/$sessionId/$statementId") + // Rename all col name to avoid duplicate columns + val colName = range(0, result.schema.size).map(x => "col" + x) + + val codec = if (SPARK_ENGINE_RUNTIME_VERSION >= "3.2") "zstd" else "zlib" + // df.write will introduce an extra shuffle for the outermost limit, and hurt performance + if (resultMaxRows > 0) { + result.toDF(colName: _*).limit(resultMaxRows).write + .option("compression", codec).format("orc").save(saveFileName.get) + } else { + result.toDF(colName: _*).write + .option("compression", codec).format("orc").save(saveFileName.get) + } + info(s"Save result to $saveFileName") + fetchOrcStatement = Some(new FetchOrcStatement(spark)) + return fetchOrcStatement.get.getIterator(saveFileName.get, resultSchema) + } val internalArray = if (resultMaxRows <= 0) { info("Execute in full collect mode") fullCollectResult(resultDF) diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/FetchOrcStatement.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/FetchOrcStatement.scala new file mode 100644 index 00000000000..861539b95b2 --- /dev/null +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/FetchOrcStatement.scala @@ -0,0 +1,151 @@ +/* + * 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.kyuubi.engine.spark.operation + +import scala.Array._ +import scala.collection.mutable.ListBuffer + +import org.apache.hadoop.conf.Configuration +import org.apache.hadoop.fs.{LocatedFileStatus, Path} +import org.apache.hadoop.mapreduce.{JobID, TaskAttemptID, TaskID, TaskType} +import org.apache.hadoop.mapreduce.lib.input.FileSplit +import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl +import org.apache.orc.mapred.OrcStruct +import org.apache.orc.mapreduce.OrcInputFormat +import org.apache.spark.sql.{Row, SparkSession} +import org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow} +import org.apache.spark.sql.catalyst.expressions.AttributeReference +import org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection +import org.apache.spark.sql.execution.datasources.RecordReaderIterator +import org.apache.spark.sql.execution.datasources.orc.OrcDeserializer +import org.apache.spark.sql.types.StructType + +import org.apache.kyuubi.KyuubiException +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.SPARK_ENGINE_RUNTIME_VERSION +import org.apache.kyuubi.operation.{FetchIterator, IterableFetchIterator} +import org.apache.kyuubi.util.reflect.DynConstructors + +class FetchOrcStatement(spark: SparkSession) { + + var orcIter: OrcFileIterator = _ + def getIterator(path: String, orcSchema: StructType): FetchIterator[Row] = { + val conf = spark.sparkContext.hadoopConfiguration + val savePath = new Path(path) + val fsIterator = savePath.getFileSystem(conf).listFiles(savePath, false) + val list = new ListBuffer[LocatedFileStatus] + while (fsIterator.hasNext) { + val file = fsIterator.next() + if (file.getPath.getName.endsWith(".orc") && file.getLen > 0) { + list += file + } + } + val toRowConverter: InternalRow => Row = { + CatalystTypeConverters.createToScalaConverter(orcSchema) + .asInstanceOf[InternalRow => Row] + } + val colId = range(0, orcSchema.size) + val fullSchema = orcSchema.map(f => + AttributeReference(f.name, f.dataType, f.nullable, f.metadata)()) + val unsafeProjection = GenerateUnsafeProjection.generate(fullSchema, fullSchema) + val deserializer = getOrcDeserializer(orcSchema, colId) + orcIter = new OrcFileIterator(list) + val iterRow = orcIter.map(value => + unsafeProjection(deserializer.deserialize(value))) + .map(value => toRowConverter(value)) + new IterableFetchIterator[Row](iterRow.toIterable) + } + + def close(): Unit = { + orcIter.close() + } + + private def getOrcDeserializer(orcSchema: StructType, colId: Array[Int]): OrcDeserializer = { + try { + if (SPARK_ENGINE_RUNTIME_VERSION >= "3.2") { + // SPARK-34535 changed the constructor signature of OrcDeserializer + DynConstructors.builder() + .impl(classOf[OrcDeserializer], classOf[StructType], classOf[Array[Int]]) + .build[OrcDeserializer]() + .newInstance( + orcSchema, + colId) + } else { + DynConstructors.builder() + .impl( + classOf[OrcDeserializer], + classOf[StructType], + classOf[StructType], + classOf[Array[Int]]) + .build[OrcDeserializer]() + .newInstance( + new StructType, + orcSchema, + colId) + } + } catch { + case e: Throwable => + throw new KyuubiException("Failed to create OrcDeserializer", e) + } + } +} + +class OrcFileIterator(fileList: ListBuffer[LocatedFileStatus]) extends Iterator[OrcStruct] { + + private val iters = fileList.map(x => getOrcFileIterator(x)) + + var idx = 0 + + override def hasNext: Boolean = { + val hasNext = iters(idx).hasNext + if (!hasNext) { + iters(idx).close() + idx += 1 + // skip empty file + while (idx < iters.size) { + if (iters(idx).hasNext) { + return true + } else { + iters(idx).close() + idx = idx + 1 + } + } + } + hasNext + } + + override def next(): OrcStruct = { + iters(idx).next() + } + + def close(): Unit = { + iters.foreach(_.close()) + } + + private def getOrcFileIterator(file: LocatedFileStatus): RecordReaderIterator[OrcStruct] = { + val orcRecordReader = { + val split = + new FileSplit(file.getPath, 0, file.getLen, Array.empty[String]) + val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP, 0), 0) + val hadoopAttemptContext = + new TaskAttemptContextImpl(new Configuration(), attemptId) + val oif = new OrcInputFormat[OrcStruct] + oif.createRecordReader(split, hadoopAttemptContext) + } + new RecordReaderIterator[OrcStruct](orcRecordReader) + } +} diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/GetTables.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/GetTables.scala index 980e4fdb173..75ce9492176 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/GetTables.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/GetTables.scala @@ -20,6 +20,7 @@ package org.apache.kyuubi.engine.spark.operation import org.apache.spark.sql.types.StructType import org.apache.kyuubi.config.KyuubiConf.OPERATION_GET_TABLES_IGNORE_TABLE_PROPERTIES +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.getSessionConf import org.apache.kyuubi.engine.spark.util.SparkCatalogUtils import org.apache.kyuubi.operation.IterableFetchIterator import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ @@ -34,10 +35,7 @@ class GetTables( extends SparkOperation(session) { protected val ignoreTableProperties = - spark.conf.getOption(OPERATION_GET_TABLES_IGNORE_TABLE_PROPERTIES.key) match { - case Some(s) => s.toBoolean - case _ => session.sessionManager.getConf.get(OPERATION_GET_TABLES_IGNORE_TABLE_PROPERTIES) - } + getSessionConf(OPERATION_GET_TABLES_IGNORE_TABLE_PROPERTIES, spark) override def statement: String = { super.statement + diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/PlanOnlyStatement.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/PlanOnlyStatement.scala index 4f88083130a..f2a67047196 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/PlanOnlyStatement.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/PlanOnlyStatement.scala @@ -27,6 +27,7 @@ import org.apache.spark.sql.types.StructType import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf.{LINEAGE_PARSER_PLUGIN_PROVIDER, OPERATION_PLAN_ONLY_EXCLUDES, OPERATION_PLAN_ONLY_OUT_STYLE} +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.getSessionConf import org.apache.kyuubi.operation.{AnalyzeMode, ArrayFetchIterator, ExecutionMode, IterableFetchIterator, JsonStyle, LineageMode, OperationHandle, OptimizeMode, OptimizeWithStatsMode, ParseMode, PhysicalMode, PlainStyle, PlanOnlyMode, PlanOnlyStyle, UnknownMode, UnknownStyle} import org.apache.kyuubi.operation.PlanOnlyMode.{notSupportedModeError, unknownModeError} import org.apache.kyuubi.operation.PlanOnlyStyle.{notSupportedStyleError, unknownStyleError} @@ -49,9 +50,7 @@ class PlanOnlyStatement( .getOrElse(session.sessionManager.getConf.get(OPERATION_PLAN_ONLY_EXCLUDES)) } - private val style = PlanOnlyStyle.fromString(spark.conf.get( - OPERATION_PLAN_ONLY_OUT_STYLE.key, - session.sessionManager.getConf.get(OPERATION_PLAN_ONLY_OUT_STYLE))) + private val style = PlanOnlyStyle.fromString(getSessionConf(OPERATION_PLAN_ONLY_OUT_STYLE, spark)) spark.conf.set(OPERATION_PLAN_ONLY_OUT_STYLE.key, style.name) override def getOperationLog: Option[OperationLog] = Option(operationLog) @@ -74,7 +73,6 @@ class PlanOnlyStatement( withLocalProperties { SQLConf.withExistingConf(spark.sessionState.conf) { val parsed = spark.sessionState.sqlParser.parsePlan(statement) - parsed match { case cmd if planExcludes.contains(cmd.getClass.getSimpleName) => result = spark.sql(statement) diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala index 1de360f0715..88ebc306b66 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala @@ -20,21 +20,20 @@ package org.apache.kyuubi.engine.spark.operation import java.io.IOException import java.time.ZoneId -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TProgressUpdateResp, TRowSet} import org.apache.spark.kyuubi.{SparkProgressMonitor, SQLOperationListener} import org.apache.spark.kyuubi.SparkUtilsHelper.redact import org.apache.spark.sql.{DataFrame, Row, SparkSession} import org.apache.spark.sql.execution.SQLExecution -import org.apache.spark.sql.types.StructType +import org.apache.spark.sql.types.{BinaryType, StructField, StructType} import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.{OPERATION_SPARK_LISTENER_ENABLED, SESSION_PROGRESS_ENABLE, SESSION_USER_SIGN_ENABLED} +import org.apache.kyuubi.config.KyuubiConf.{ARROW_BASED_ROWSET_TIMESTAMP_AS_STRING, ENGINE_SPARK_OUTPUT_MODE, EngineSparkOutputMode, OPERATION_SPARK_LISTENER_ENABLED, SESSION_PROGRESS_ENABLE, SESSION_USER_SIGN_ENABLED} import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_SESSION_SIGN_PUBLICKEY, KYUUBI_SESSION_USER_KEY, KYUUBI_SESSION_USER_SIGN, KYUUBI_STATEMENT_ID_KEY} -import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.SPARK_SCHEDULER_POOL_KEY +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.{getSessionConf, SPARK_SCHEDULER_POOL_KEY} import org.apache.kyuubi.engine.spark.events.SparkOperationEvent import org.apache.kyuubi.engine.spark.operation.SparkOperation.TIMEZONE_KEY -import org.apache.kyuubi.engine.spark.schema.{RowSet, SchemaHelper} +import org.apache.kyuubi.engine.spark.schema.{SchemaHelper, SparkArrowTRowSetGenerator, SparkTRowSetGenerator} import org.apache.kyuubi.engine.spark.session.SparkSessionImpl import org.apache.kyuubi.events.EventBus import org.apache.kyuubi.operation.{AbstractOperation, FetchIterator, OperationState, OperationStatus} @@ -42,6 +41,8 @@ import org.apache.kyuubi.operation.FetchOrientation._ import org.apache.kyuubi.operation.OperationState.OperationState import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TProgressUpdateResp, TRowSet} +import org.apache.kyuubi.util.ThriftUtils abstract class SparkOperation(session: Session) extends AbstractOperation(session) { @@ -63,11 +64,8 @@ abstract class SparkOperation(session: Session) override def redactedStatement: String = redact(spark.sessionState.conf.stringRedactionPattern, statement) - protected val operationSparkListenerEnabled = - spark.conf.getOption(OPERATION_SPARK_LISTENER_ENABLED.key) match { - case Some(s) => s.toBoolean - case _ => session.sessionManager.getConf.get(OPERATION_SPARK_LISTENER_ENABLED) - } + protected val operationSparkListenerEnabled: Boolean = + getSessionConf(OPERATION_SPARK_LISTENER_ENABLED, spark) protected val operationListener: Option[SQLOperationListener] = if (operationSparkListenerEnabled) { @@ -80,13 +78,13 @@ abstract class SparkOperation(session: Session) operationListener.foreach(spark.sparkContext.addSparkListener(_)) } - private val progressEnable = spark.conf.getOption(SESSION_PROGRESS_ENABLE.key) match { - case Some(s) => s.toBoolean - case _ => session.sessionManager.getConf.get(SESSION_PROGRESS_ENABLE) - } + private val progressEnable: Boolean = getSessionConf(SESSION_PROGRESS_ENABLE, spark) protected def supportProgress: Boolean = false + protected def outputMode: EngineSparkOutputMode.EngineSparkOutputMode = + EngineSparkOutputMode.withName(getSessionConf(ENGINE_SPARK_OUTPUT_MODE, spark)) + override def getStatus: OperationStatus = { if (progressEnable && supportProgress) { val progressMonitor = new SparkProgressMonitor(spark, statementId) @@ -113,9 +111,7 @@ abstract class SparkOperation(session: Session) protected val forceCancel = session.sessionManager.getConf.get(KyuubiConf.OPERATION_FORCE_CANCEL) - protected val schedulerPool = - spark.conf.getOption(KyuubiConf.OPERATION_SCHEDULER_POOL.key).orElse( - session.sessionManager.getConf.get(KyuubiConf.OPERATION_SCHEDULER_POOL)) + protected val schedulerPool = getSessionConf(KyuubiConf.OPERATION_SCHEDULER_POOL, spark) protected val isSessionUserSignEnabled: Boolean = spark.sparkContext.getConf.getBoolean( s"spark.${SESSION_USER_SIGN_ENABLED.key}", @@ -251,13 +247,16 @@ abstract class SparkOperation(session: Session) if (isArrowBasedOperation) { if (iter.hasNext) { val taken = iter.next().asInstanceOf[Array[Byte]] - RowSet.toTRowSet(taken, getProtocolVersion) + new SparkArrowTRowSetGenerator().toTRowSet( + Seq(taken), + new StructType().add(StructField(null, BinaryType)), + getProtocolVersion) } else { - RowSet.emptyTRowSet() + ThriftUtils.newEmptyRowSet } } else { val taken = iter.take(rowSetSize) - RowSet.toTRowSet( + new SparkTRowSetGenerator().toTRowSet( taken.toSeq.asInstanceOf[Seq[Row]], resultSchema, getProtocolVersion) @@ -279,7 +278,7 @@ abstract class SparkOperation(session: Session) protected def resultFormat: String = "thrift" protected def timestampAsString: Boolean = { - spark.conf.get("kyuubi.operation.result.arrow.timestampAsString", "false").toBoolean + spark.conf.get(ARROW_BASED_ROWSET_TIMESTAMP_AS_STRING.key, "false").toBoolean } protected def setSessionUserSign(): Unit = { diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkSQLOperationManager.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkSQLOperationManager.scala index ab082874630..cd365c62a6f 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkSQLOperationManager.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkSQLOperationManager.scala @@ -86,7 +86,7 @@ class SparkSQLOperationManager private (name: String) extends OperationManager(n val incrementalCollect = spark.conf.getOption(OPERATION_INCREMENTAL_COLLECT.key) .map(_.toBoolean).getOrElse(operationIncrementalCollectDefault) // TODO: respect the config of the operation ExecuteStatement, if it was set. - val resultFormat = spark.conf.get("kyuubi.operation.result.format", "thrift") + val resultFormat = spark.conf.get(OPERATION_RESULT_FORMAT.key, "thrift") resultFormat.toLowerCase match { case "arrow" => new ArrowBasedExecuteStatement( diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/RowSet.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/RowSet.scala index 4f935ce49f0..c5f32210891 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/RowSet.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/RowSet.scala @@ -17,231 +17,17 @@ package org.apache.kyuubi.engine.spark.schema -import java.nio.ByteBuffer - -import scala.collection.JavaConverters._ - -import org.apache.hive.service.rpc.thrift._ -import org.apache.spark.sql.Row import org.apache.spark.sql.execution.HiveResult +import org.apache.spark.sql.execution.HiveResult.TimeFormatters import org.apache.spark.sql.types._ -import org.apache.kyuubi.util.RowSetUtils._ - object RowSet { - def toHiveString(valueAndType: (Any, DataType), nested: Boolean = false): String = { - // compatible w/ Spark 3.1 and above - val timeFormatters = HiveResult.getTimeFormatters + def toHiveString( + valueAndType: (Any, DataType), + nested: Boolean = false, + timeFormatters: TimeFormatters): String = { HiveResult.toHiveString(valueAndType, nested, timeFormatters) } - def toTRowSet( - bytes: Array[Byte], - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - throw new UnsupportedOperationException - } else { - toColumnBasedSet(bytes) - } - } - - def emptyTRowSet(): TRowSet = { - new TRowSet(0, new java.util.ArrayList[TRow](0)) - } - - def toColumnBasedSet(data: Array[Byte]): TRowSet = { - val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](1)) - val tColumn = toTColumn(data) - tRowSet.addToColumns(tColumn) - tRowSet - } - - def toTRowSet( - rows: Seq[Row], - schema: StructType, - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBasedSet(rows, schema) - } else { - toColumnBasedSet(rows, schema) - } - } - - def toRowBasedSet(rows: Seq[Row], schema: StructType): TRowSet = { - val rowSize = rows.length - val tRows = new java.util.ArrayList[TRow](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - var j = 0 - val columnSize = row.length - while (j < columnSize) { - val columnValue = toTColumnValue(j, row, schema) - tRow.addToColVals(columnValue) - j += 1 - } - i += 1 - tRows.add(tRow) - } - new TRowSet(0, tRows) - } - - def toColumnBasedSet(rows: Seq[Row], schema: StructType): TRowSet = { - val rowSize = rows.length - val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](rowSize)) - var i = 0 - val columnSize = schema.length - while (i < columnSize) { - val field = schema(i) - val tColumn = toTColumn(rows, i, field.dataType) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - private def toTColumn(rows: Seq[Row], ordinal: Int, typ: DataType): TColumn = { - val nulls = new java.util.BitSet() - typ match { - case BooleanType => - val values = getOrSetAsNull[java.lang.Boolean](rows, ordinal, nulls, true) - TColumn.boolVal(new TBoolColumn(values, nulls)) - - case ByteType => - val values = getOrSetAsNull[java.lang.Byte](rows, ordinal, nulls, 0.toByte) - TColumn.byteVal(new TByteColumn(values, nulls)) - - case ShortType => - val values = getOrSetAsNull[java.lang.Short](rows, ordinal, nulls, 0.toShort) - TColumn.i16Val(new TI16Column(values, nulls)) - - case IntegerType => - val values = getOrSetAsNull[java.lang.Integer](rows, ordinal, nulls, 0) - TColumn.i32Val(new TI32Column(values, nulls)) - - case LongType => - val values = getOrSetAsNull[java.lang.Long](rows, ordinal, nulls, 0L) - TColumn.i64Val(new TI64Column(values, nulls)) - - case FloatType => - val values = getOrSetAsNull[java.lang.Float](rows, ordinal, nulls, 0.toFloat) - .asScala.map(n => java.lang.Double.valueOf(n.toString)).asJava - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case DoubleType => - val values = getOrSetAsNull[java.lang.Double](rows, ordinal, nulls, 0.toDouble) - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case StringType => - val values = getOrSetAsNull[java.lang.String](rows, ordinal, nulls, "") - TColumn.stringVal(new TStringColumn(values, nulls)) - - case BinaryType => - val values = getOrSetAsNull[Array[Byte]](rows, ordinal, nulls, Array()) - .asScala - .map(ByteBuffer.wrap) - .asJava - TColumn.binaryVal(new TBinaryColumn(values, nulls)) - - case _ => - var i = 0 - val rowSize = rows.length - val values = new java.util.ArrayList[String](rowSize) - while (i < rowSize) { - val row = rows(i) - nulls.set(i, row.isNullAt(ordinal)) - values.add(toHiveString(row.get(ordinal) -> typ)) - i += 1 - } - TColumn.stringVal(new TStringColumn(values, nulls)) - } - } - - private def getOrSetAsNull[T]( - rows: Seq[Row], - ordinal: Int, - nulls: java.util.BitSet, - defaultVal: T): java.util.List[T] = { - val size = rows.length - val ret = new java.util.ArrayList[T](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row.isNullAt(ordinal) - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row.getAs[T](ordinal)) - } - idx += 1 - } - ret - } - - private def toTColumnValue( - ordinal: Int, - row: Row, - types: StructType): TColumnValue = { - types(ordinal).dataType match { - case BooleanType => - val boolValue = new TBoolValue - if (!row.isNullAt(ordinal)) boolValue.setValue(row.getBoolean(ordinal)) - TColumnValue.boolVal(boolValue) - - case ByteType => - val byteValue = new TByteValue - if (!row.isNullAt(ordinal)) byteValue.setValue(row.getByte(ordinal)) - TColumnValue.byteVal(byteValue) - - case ShortType => - val tI16Value = new TI16Value - if (!row.isNullAt(ordinal)) tI16Value.setValue(row.getShort(ordinal)) - TColumnValue.i16Val(tI16Value) - - case IntegerType => - val tI32Value = new TI32Value - if (!row.isNullAt(ordinal)) tI32Value.setValue(row.getInt(ordinal)) - TColumnValue.i32Val(tI32Value) - - case LongType => - val tI64Value = new TI64Value - if (!row.isNullAt(ordinal)) tI64Value.setValue(row.getLong(ordinal)) - TColumnValue.i64Val(tI64Value) - - case FloatType => - val tDoubleValue = new TDoubleValue - if (!row.isNullAt(ordinal)) { - val doubleValue = java.lang.Double.valueOf(row.getFloat(ordinal).toString) - tDoubleValue.setValue(doubleValue) - } - TColumnValue.doubleVal(tDoubleValue) - - case DoubleType => - val tDoubleValue = new TDoubleValue - if (!row.isNullAt(ordinal)) tDoubleValue.setValue(row.getDouble(ordinal)) - TColumnValue.doubleVal(tDoubleValue) - - case StringType => - val tStringValue = new TStringValue - if (!row.isNullAt(ordinal)) tStringValue.setValue(row.getString(ordinal)) - TColumnValue.stringVal(tStringValue) - - case _ => - val tStrValue = new TStringValue - if (!row.isNullAt(ordinal)) { - tStrValue.setValue(toHiveString(row.get(ordinal) -> types(ordinal).dataType)) - } - TColumnValue.stringVal(tStrValue) - } - } - - private def toTColumn(data: Array[Byte]): TColumn = { - val values = new java.util.ArrayList[ByteBuffer](1) - values.add(ByteBuffer.wrap(data)) - val nulls = new java.util.BitSet() - TColumn.binaryVal(new TBinaryColumn(values, nulls)) - } } diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelper.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelper.scala index 8db46e2b7f4..3da5937015c 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelper.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelper.scala @@ -21,9 +21,10 @@ import java.util.Collections import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ import org.apache.spark.sql.types._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + object SchemaHelper { /** @@ -140,6 +141,8 @@ object SchemaHelper { case dt if Array(TIMESTAMP_NTZ, DAY_TIME_INTERVAL, YEAR_MONTH_INTERVAL) .contains(dt.getClass.getSimpleName) => Some(dt.defaultSize) + case dt: DecimalType => + Some(dt.precision) case dt @ (BooleanType | _: NumericType | DateType | TimestampType | CalendarIntervalType | NullType) => Some(dt.defaultSize) diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkArrowTRowSetGenerator.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkArrowTRowSetGenerator.scala new file mode 100644 index 00000000000..054df0dd653 --- /dev/null +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkArrowTRowSetGenerator.scala @@ -0,0 +1,75 @@ +/* + * 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.kyuubi.engine.spark.schema + +import java.nio.ByteBuffer + +import org.apache.spark.sql.types._ + +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class SparkArrowTRowSetGenerator + extends TRowSetGenerator[StructType, Array[Byte], DataType] { + override def toColumnBasedSet(rows: Seq[Array[Byte]], schema: StructType): TRowSet = { + require(schema.length == 1, "ArrowRowSetGenerator accepts only one single byte array") + require(schema.head.dataType == BinaryType, "ArrowRowSetGenerator accepts only BinaryType") + + val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](1)) + val tColumn = toTColumn(rows, 1, schema.head.dataType) + tRowSet.addToColumns(tColumn) + tRowSet + } + + override def toTColumn(rows: Seq[Array[Byte]], ordinal: Int, typ: DataType): TColumn = { + require(rows.length == 1, "ArrowRowSetGenerator accepts only one single byte array") + typ match { + case BinaryType => + val values = new java.util.ArrayList[ByteBuffer](1) + values.add(ByteBuffer.wrap(rows.head)) + TColumn.binaryVal(new TBinaryColumn(values, ByteBuffer.wrap(Array[Byte]()))) + case _ => throw new IllegalArgumentException( + s"unsupported datatype $typ, ArrowRowSetGenerator accepts only BinaryType") + } + } + + override def toRowBasedSet(rows: Seq[Array[Byte]], schema: StructType): TRowSet = { + throw new UnsupportedOperationException + } + + override def getColumnSizeFromSchemaType(schema: StructType): Int = { + throw new UnsupportedOperationException + } + + override def getColumnType(schema: StructType, ordinal: Int): DataType = { + throw new UnsupportedOperationException + } + + override def isColumnNullAt(row: Array[Byte], ordinal: Int): Boolean = { + throw new UnsupportedOperationException + } + + override def getColumnAs[T](row: Array[Byte], ordinal: Int): T = { + throw new UnsupportedOperationException + } + + override def toTColumnValue(row: Array[Byte], ordinal: Int, types: StructType): TColumnValue = { + throw new UnsupportedOperationException + } + +} diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkTRowSetGenerator.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkTRowSetGenerator.scala new file mode 100644 index 00000000000..1d1b5ef6aab --- /dev/null +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/schema/SparkTRowSetGenerator.scala @@ -0,0 +1,82 @@ +/* + * 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.kyuubi.engine.spark.schema + +import org.apache.spark.sql.Row +import org.apache.spark.sql.execution.HiveResult +import org.apache.spark.sql.types._ + +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class SparkTRowSetGenerator + extends TRowSetGenerator[StructType, Row, DataType] { + + // reused time formatters in single RowSet generation, see KYUUBI-5811 + private val tf = HiveResult.getTimeFormatters + + override def getColumnSizeFromSchemaType(schema: StructType): Int = schema.length + + override def getColumnType(schema: StructType, ordinal: Int): DataType = schema(ordinal).dataType + + override def isColumnNullAt(row: Row, ordinal: Int): Boolean = row.isNullAt(ordinal) + + override def getColumnAs[T](row: Row, ordinal: Int): T = row.getAs[T](ordinal) + + override def toTColumn(rows: Seq[Row], ordinal: Int, typ: DataType): TColumn = { + typ match { + case BooleanType => asBooleanTColumn(rows, ordinal) + case ByteType => asByteTColumn(rows, ordinal) + case ShortType => asShortTColumn(rows, ordinal) + case IntegerType => asIntegerTColumn(rows, ordinal) + case LongType => asLongTColumn(rows, ordinal) + case FloatType => asFloatTColumn(rows, ordinal) + case DoubleType => asDoubleTColumn(rows, ordinal) + case StringType => asStringTColumn(rows, ordinal) + case BinaryType => asByteArrayTColumn(rows, ordinal) + case _ => + val timeFormatters = tf + asStringTColumn( + rows, + ordinal, + "NULL", + (row, ordinal) => + RowSet.toHiveString( + getColumnAs[Any](row, ordinal) -> typ, + timeFormatters = timeFormatters)) + } + } + + override def toTColumnValue(row: Row, ordinal: Int, types: StructType): TColumnValue = { + getColumnType(types, ordinal) match { + case BooleanType => asBooleanTColumnValue(row, ordinal) + case ByteType => asByteTColumnValue(row, ordinal) + case ShortType => asShortTColumnValue(row, ordinal) + case IntegerType => asIntegerTColumnValue(row, ordinal) + case LongType => asLongTColumnValue(row, ordinal) + case FloatType => asFloatTColumnValue(row, ordinal) + case DoubleType => asDoubleTColumnValue(row, ordinal) + case StringType => asStringTColumnValue(row, ordinal) + case _ => asStringTColumnValue( + row, + ordinal, + rawValue => RowSet.toHiveString(rawValue -> types(ordinal).dataType, timeFormatters = tf)) + } + } + +} diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSQLSessionManager.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSQLSessionManager.scala index 79f38ce35a4..aab2d51068f 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSQLSessionManager.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSQLSessionManager.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.engine.spark.session import java.util.concurrent.{ScheduledExecutorService, TimeUnit} -import org.apache.hive.service.rpc.thrift.TProtocolVersion +import org.apache.hadoop.fs.Path import org.apache.spark.api.python.KyuubiPythonGatewayServer import org.apache.spark.sql.SparkSession @@ -29,9 +29,12 @@ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.ShareLevel import org.apache.kyuubi.engine.ShareLevel._ import org.apache.kyuubi.engine.spark.{KyuubiSparkUtil, SparkSQLEngine} +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.engineId import org.apache.kyuubi.engine.spark.operation.SparkSQLOperationManager import org.apache.kyuubi.session._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.util.ThreadUtils +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay /** * A [[SessionManager]] constructed with [[SparkSession]] which give it the ability to talk with @@ -66,8 +69,9 @@ class SparkSQLSessionManager private (name: String, spark: SparkSession) if (!userIsolatedSparkSession) { userIsolatedSparkSessionThread = Some(ThreadUtils.newDaemonSingleThreadScheduledExecutor("user-isolated-cache-checker")) - userIsolatedSparkSessionThread.foreach { - _.scheduleWithFixedDelay( + userIsolatedSparkSessionThread.foreach { thread => + scheduleTolerableRunnableWithFixedDelay( + thread, () => { userIsolatedCacheLock.synchronized { val iter = userIsolatedCacheCount.entrySet().iterator() @@ -128,7 +132,9 @@ class SparkSQLSessionManager private (name: String, spark: SparkSession) private def newSparkSession(rootSparkSession: SparkSession): SparkSession = { val newSparkSession = rootSparkSession.newSession() - KyuubiSparkUtil.initializeSparkSession(newSparkSession, conf.get(ENGINE_SESSION_INITIALIZE_SQL)) + KyuubiSparkUtil.initializeSparkSession( + newSparkSession, + conf.get(ENGINE_SESSION_SPARK_INITIALIZE_SQL)) newSparkSession } @@ -180,6 +186,12 @@ class SparkSQLSessionManager private (name: String, spark: SparkSession) info("Session stopped due to shared level is Connection.") stopSession() } + if (conf.get(OPERATION_RESULT_SAVE_TO_FILE)) { + val path = new Path(s"${conf.get(OPERATION_RESULT_SAVE_TO_FILE_DIR)}/" + + s"$engineId/${sessionHandle.identifier}") + path.getFileSystem(spark.sparkContext.hadoopConfiguration).delete(path, true) + info(s"Delete session result file $path") + } } private def stopSession(): Unit = { diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala index 8d9012cbdc6..08bd09b4483 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala @@ -18,7 +18,6 @@ package org.apache.kyuubi.engine.spark.session import org.apache.commons.lang3.StringUtils -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} import org.apache.spark.sql.{AnalysisException, SparkSession} import org.apache.kyuubi.KyuubiSQLException @@ -30,6 +29,7 @@ import org.apache.kyuubi.engine.spark.util.SparkCatalogUtils import org.apache.kyuubi.events.EventBus import org.apache.kyuubi.operation.{Operation, OperationHandle} import org.apache.kyuubi.session._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} class SparkSessionImpl( protocol: TProtocolVersion, diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/util/JsonUtils.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/util/JsonUtils.scala new file mode 100644 index 00000000000..192c6dbb40c --- /dev/null +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/util/JsonUtils.scala @@ -0,0 +1,47 @@ +/* + * 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.kyuubi.engine.spark.util + +import com.fasterxml.jackson.databind.{DeserializationFeature, JsonNode, ObjectMapper} +import com.fasterxml.jackson.module.scala.DefaultScalaModule + +object JsonUtils { + val mapper: ObjectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(DefaultScalaModule) + + def toJson[T](obj: T): String = { + mapper.writeValueAsString(obj) + } + + def toPrettyJson[T](obj: T): String = { + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj) + } + + def fromJson[T](json: String, clz: Class[T]): T = { + mapper.readValue(json, clz) + } + + def fromJson[T](json: String)(implicit m: Manifest[T]): T = { + mapper.readValue(json, m.runtimeClass).asInstanceOf[T] + } + + def readTree(content: String): JsonNode = { + mapper.readTree(content) + } +} diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SQLOperationListener.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SQLOperationListener.scala index 686cb1f359b..a7d409c7ca5 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SQLOperationListener.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SQLOperationListener.scala @@ -27,10 +27,9 @@ import org.apache.spark.sql.SparkSession import org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionEnd import org.apache.kyuubi.Logging -import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SPARK_SHOW_PROGRESS, ENGINE_SPARK_SHOW_PROGRESS_TIME_FORMAT, ENGINE_SPARK_SHOW_PROGRESS_UPDATE_INTERVAL} import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_STATEMENT_ID_KEY -import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.SPARK_SQL_EXECUTION_ID_KEY +import org.apache.kyuubi.engine.spark.KyuubiSparkUtil.{getSessionConf, SPARK_SQL_EXECUTION_ID_KEY} import org.apache.kyuubi.engine.spark.operation.ExecuteStatement import org.apache.kyuubi.operation.Operation import org.apache.kyuubi.operation.log.OperationLog @@ -50,15 +49,14 @@ class SQLOperationListener( private lazy val activeStages = new ConcurrentHashMap[SparkStageAttempt, SparkStageInfo]() private var executionId: Option[Long] = None - private val conf: KyuubiConf = operation.getSession.sessionManager.getConf private lazy val consoleProgressBar = - if (conf.get(ENGINE_SPARK_SHOW_PROGRESS)) { + if (getSessionConf(ENGINE_SPARK_SHOW_PROGRESS, spark)) { Some(new SparkConsoleProgressBar( operation, activeJobs, activeStages, - conf.get(ENGINE_SPARK_SHOW_PROGRESS_UPDATE_INTERVAL), - conf.get(ENGINE_SPARK_SHOW_PROGRESS_TIME_FORMAT))) + getSessionConf(ENGINE_SPARK_SHOW_PROGRESS_UPDATE_INTERVAL, spark), + getSessionConf(ENGINE_SPARK_SHOW_PROGRESS_TIME_FORMAT, spark))) } else { None } diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SparkProgressMonitor.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SparkProgressMonitor.scala index 1d9ef53eae9..80cf292755c 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SparkProgressMonitor.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/kyuubi/SparkProgressMonitor.scala @@ -21,12 +21,12 @@ import java.util import scala.collection.JavaConverters._ import scala.collection.immutable.SortedMap -import org.apache.hive.service.rpc.thrift.TJobExecutionStatus import org.apache.spark.kyuubi.SparkProgressMonitor._ import org.apache.spark.sql.SparkSession import org.apache.spark.status.api.v1.StageStatus import org.apache.kyuubi.engine.spark.operation.progress.{SparkOperationProgressStatus, SparkStage, SparkStageProgress} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TJobExecutionStatus class SparkProgressMonitor(spark: SparkSession, jobGroup: String) { diff --git a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/sql/kyuubi/SparkDatasetHelper.scala b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/sql/kyuubi/SparkDatasetHelper.scala index c0f9d61c210..16f597cdb34 100644 --- a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/sql/kyuubi/SparkDatasetHelper.scala +++ b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/sql/kyuubi/SparkDatasetHelper.scala @@ -25,7 +25,9 @@ import org.apache.spark.network.util.{ByteUnit, JavaUtils} import org.apache.spark.rdd.RDD import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession} import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.plans.logical.statsEstimation.EstimationUtils import org.apache.spark.sql.execution.{CollectLimitExec, LocalTableScanExec, SparkPlan, SQLExecution} +import org.apache.spark.sql.execution.HiveResult import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanExec import org.apache.spark.sql.execution.arrow.KyuubiArrowConverters import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics} @@ -105,17 +107,31 @@ object SparkDatasetHelper extends Logging { val quotedCol = (name: String) => col(quoteIfNeeded(name)) // an udf to call `RowSet.toHiveString` on complex types(struct/array/map) and timestamp type. + // TODO: reuse the timeFormatters on greater scale if possible, + // recreating timeFormatters may cause performance issue, see [KYUUBI#5811] val toHiveStringUDF = udf[String, Row, String]((row, schemaDDL) => { val dt = DataType.fromDDL(schemaDDL) dt match { case StructType(Array(StructField(_, st: StructType, _, _))) => - RowSet.toHiveString((row, st), nested = true) + RowSet.toHiveString( + (row, st), + nested = true, + timeFormatters = HiveResult.getTimeFormatters) case StructType(Array(StructField(_, at: ArrayType, _, _))) => - RowSet.toHiveString((row.toSeq.head, at), nested = true) + RowSet.toHiveString( + (row.toSeq.head, at), + nested = true, + timeFormatters = HiveResult.getTimeFormatters) case StructType(Array(StructField(_, mt: MapType, _, _))) => - RowSet.toHiveString((row.toSeq.head, mt), nested = true) + RowSet.toHiveString( + (row.toSeq.head, mt), + nested = true, + timeFormatters = HiveResult.getTimeFormatters) case StructType(Array(StructField(_, tt: TimestampType, _, _))) => - RowSet.toHiveString((row.toSeq.head, tt), nested = true) + RowSet.toHiveString( + (row.toSeq.head, tt), + nested = true, + timeFormatters = HiveResult.getTimeFormatters) case _ => throw new UnsupportedOperationException } @@ -278,4 +294,32 @@ object SparkDatasetHelper extends Logging { val executionId = sc.getLocalProperty(SQLExecution.EXECUTION_ID_KEY) SQLMetrics.postDriverMetricUpdates(sc, executionId, metrics.values.toSeq) } + + def shouldSaveResultToFs(resultMaxRows: Int, minSize: Long, result: DataFrame): Boolean = { + if (isCommandExec(result.queryExecution.executedPlan.nodeName)) { + return false + } + lazy val limit = result.queryExecution.executedPlan match { + case collectLimit: CollectLimitExec => collectLimit.limit + case _ => resultMaxRows + } + lazy val stats = if (limit > 0) { + limit * EstimationUtils.getSizePerRow( + result.queryExecution.executedPlan.output) + } else { + result.queryExecution.optimizedPlan.stats.sizeInBytes + } + lazy val colSize = + if (result == null || result.schema.isEmpty) { + 0 + } else { + result.schema.size + } + minSize > 0 && colSize > 0 && stats >= minSize + } + + private def isCommandExec(nodeName: String): Boolean = { + nodeName == "org.apache.spark.sql.execution.command.ExecutedCommandExec" || + nodeName == "org.apache.spark.sql.execution.CommandResultExec" + } } diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/IndividualSparkSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/IndividualSparkSuite.scala index 8fca1d0ca2b..e924aa3de49 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/IndividualSparkSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/IndividualSparkSuite.scala @@ -104,13 +104,17 @@ class SparkEngineSuites extends KyuubiFunSuite { withSystemProperty(Map( s"spark.$KYUUBI_ENGINE_SUBMIT_TIME_KEY" -> String.valueOf(submitTime), s"spark.${ENGINE_INIT_TIMEOUT.key}" -> String.valueOf(timeout), - s"spark.${ENGINE_INITIALIZE_SQL.key}" -> + s"spark.${ENGINE_SPARK_INITIALIZE_SQL.key}" -> "select 1 where java_method('java.lang.Thread', 'sleep', 60000L) is null")) { SparkSQLEngine.setupConf() SparkSQLEngine.currentEngine = None val logAppender = new LogAppender("test createSpark timeout") withLogAppender(logAppender) { - SparkSQLEngine.main(Array.empty) + try { + SparkSQLEngine.main(Array.empty) + } catch { + case e: Exception => error("", e) + } } assert(SparkSQLEngine.currentEngine.isEmpty) val errorMsg = s"The Engine main thread was interrupted, possibly due to `createSpark`" + diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/events/handler/SparkJsonLoggingEventHandlerSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/events/handler/SparkJsonLoggingEventHandlerSuite.scala index ddaa962193b..e7e6768e7af 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/events/handler/SparkJsonLoggingEventHandlerSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/events/handler/SparkJsonLoggingEventHandlerSuite.scala @@ -22,7 +22,6 @@ import java.nio.file.Paths import org.apache.hadoop.conf.Configuration import org.apache.hadoop.fs.{FileSystem, FSDataInputStream, Path} -import org.apache.hive.service.rpc.thrift.TExecuteStatementReq import org.scalatest.time.SpanSugar._ import org.apache.kyuubi.Utils @@ -32,6 +31,7 @@ import org.apache.kyuubi.engine.spark.events.{EngineEvent, SessionEvent} import org.apache.kyuubi.events.EventLoggerType._ import org.apache.kyuubi.events.JsonProtocol import org.apache.kyuubi.operation.{HiveJDBCTestHelper, OperationHandle} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TExecuteStatementReq class SparkJsonLoggingEventHandlerSuite extends WithSparkSQLEngine with HiveJDBCTestHelper { diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationProgressSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationProgressSuite.scala index a82443f41a1..def45d3873c 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationProgressSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationProgressSuite.scala @@ -19,12 +19,12 @@ package org.apache.kyuubi.engine.spark.operation import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TGetOperationStatusReq, TJobExecutionStatus} import org.scalatest.concurrent.PatienceConfiguration.Timeout import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.engine.spark.WithSparkSQLEngine import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TGetOperationStatusReq, TJobExecutionStatus} class SparkOperationProgressSuite extends WithSparkSQLEngine with HiveJDBCTestHelper { diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationSuite.scala index adab0231d63..fb9873fd05f 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/operation/SparkOperationSuite.scala @@ -25,7 +25,6 @@ import org.apache.hadoop.hive.thrift.{DelegationTokenIdentifier => HiveTokenIden import org.apache.hadoop.io.Text import org.apache.hadoop.security.{Credentials, UserGroupInformation} import org.apache.hadoop.security.token.{Token, TokenIdentifier} -import org.apache.hive.service.rpc.thrift._ import org.apache.spark.SPARK_VERSION import org.apache.spark.kyuubi.SparkContextHelper import org.apache.spark.sql.catalyst.analysis.FunctionRegistry @@ -38,6 +37,7 @@ import org.apache.kyuubi.engine.spark.util.SparkCatalogUtils import org.apache.kyuubi.jdbc.hive.KyuubiStatement import org.apache.kyuubi.operation.{HiveMetadataTests, SparkQueryTests} import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ import org.apache.kyuubi.util.KyuubiHadoopUtils import org.apache.kyuubi.util.SemanticVersion @@ -154,6 +154,7 @@ class SparkOperationSuite extends WithSparkSQLEngine with HiveMetadataTests with val colSize = rowSet.getInt(COLUMN_SIZE) schema(pos).dataType match { case StringType | BinaryType | _: ArrayType | _: MapType => assert(colSize === 0) + case d: DecimalType => assert(colSize === d.precision) case StructType(fields) if fields.length == 1 => assert(colSize === 0) case o => assert(colSize === o.defaultSize) } diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/RowSetSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/RowSetSuite.scala index 5d2ba4a0d11..228bdcaf2c0 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/RowSetSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/RowSetSuite.scala @@ -24,12 +24,13 @@ import java.time.{Instant, LocalDate} import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.spark.sql.Row +import org.apache.spark.sql.execution.HiveResult import org.apache.spark.sql.types._ import org.apache.spark.unsafe.types.CalendarInterval import org.apache.kyuubi.KyuubiFunSuite +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class RowSetSuite extends KyuubiFunSuite { @@ -98,7 +99,7 @@ class RowSetSuite extends KyuubiFunSuite { private val rows: Seq[Row] = (0 to 10).map(genRow) ++ Seq(Row.fromSeq(Seq.fill(17)(null))) test("column based set") { - val tRowSet = RowSet.toColumnBasedSet(rows, schema) + val tRowSet = new SparkTRowSetGenerator().toColumnBasedSet(rows, schema) assert(tRowSet.getColumns.size() === schema.size) assert(tRowSet.getRowsSize === 0) @@ -165,14 +166,18 @@ class RowSetSuite extends KyuubiFunSuite { dateCol.getValues.asScala.zipWithIndex.foreach { case (b, 11) => assert(b === "NULL") case (b, i) => - assert(b === RowSet.toHiveString(Date.valueOf(s"2018-11-${i + 1}") -> DateType)) + assert(b === RowSet.toHiveString( + Date.valueOf(s"2018-11-${i + 1}") -> DateType, + timeFormatters = HiveResult.getTimeFormatters)) } val tsCol = cols.next().getStringVal tsCol.getValues.asScala.zipWithIndex.foreach { case (b, 11) => assert(b === "NULL") case (b, i) => assert(b === - RowSet.toHiveString(Timestamp.valueOf(s"2018-11-17 13:33:33.$i") -> TimestampType)) + RowSet.toHiveString( + Timestamp.valueOf(s"2018-11-17 13:33:33.$i") -> TimestampType, + timeFormatters = HiveResult.getTimeFormatters)) } val binCol = cols.next().getBinaryVal @@ -185,14 +190,16 @@ class RowSetSuite extends KyuubiFunSuite { arrCol.getValues.asScala.zipWithIndex.foreach { case (b, 11) => assert(b === "NULL") case (b, i) => assert(b === RowSet.toHiveString( - Array.fill(i)(java.lang.Double.valueOf(s"$i.$i")).toSeq -> ArrayType(DoubleType))) + Array.fill(i)(java.lang.Double.valueOf(s"$i.$i")).toSeq -> ArrayType(DoubleType), + timeFormatters = HiveResult.getTimeFormatters)) } val mapCol = cols.next().getStringVal mapCol.getValues.asScala.zipWithIndex.foreach { case (b, 11) => assert(b === "NULL") case (b, i) => assert(b === RowSet.toHiveString( - Map(i -> java.lang.Double.valueOf(s"$i.$i")) -> MapType(IntegerType, DoubleType))) + Map(i -> java.lang.Double.valueOf(s"$i.$i")) -> MapType(IntegerType, DoubleType), + timeFormatters = HiveResult.getTimeFormatters)) } val intervalCol = cols.next().getStringVal @@ -203,7 +210,7 @@ class RowSetSuite extends KyuubiFunSuite { } test("row based set") { - val tRowSet = RowSet.toRowBasedSet(rows, schema) + val tRowSet = new SparkTRowSetGenerator().toRowBasedSet(rows, schema) assert(tRowSet.getColumnCount === 0) assert(tRowSet.getRowsSize === rows.size) val iter = tRowSet.getRowsIterator @@ -241,7 +248,9 @@ class RowSetSuite extends KyuubiFunSuite { val r8 = iter.next().getColVals assert(r8.get(12).getStringVal.getValue === Array.fill(7)(7.7d).mkString("[", ",", "]")) assert(r8.get(13).getStringVal.getValue === - RowSet.toHiveString(Map(7 -> 7.7d) -> MapType(IntegerType, DoubleType))) + RowSet.toHiveString( + Map(7 -> 7.7d) -> MapType(IntegerType, DoubleType), + timeFormatters = HiveResult.getTimeFormatters)) val r9 = iter.next().getColVals assert(r9.get(14).getStringVal.getValue === new CalendarInterval(8, 8, 8).toString) @@ -249,7 +258,7 @@ class RowSetSuite extends KyuubiFunSuite { test("to row set") { TProtocolVersion.values().foreach { proto => - val set = RowSet.toTRowSet(rows, schema, proto) + val set = new SparkTRowSetGenerator().toTRowSet(rows, schema, proto) if (proto.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { assert(!set.isSetColumns, proto.toString) assert(set.isSetRows, proto.toString) diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelperSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelperSuite.scala index 6bd0364f754..b2514e7dd73 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelperSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/schema/SchemaHelperSuite.scala @@ -21,11 +21,11 @@ import java.time.ZoneId import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TCLIServiceConstants, TTypeId} import org.apache.spark.sql.types._ import org.apache.kyuubi.KyuubiFunSuite import org.apache.kyuubi.engine.spark.schema.SchemaHelper._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TCLIServiceConstants, TTypeId} class SchemaHelperSuite extends KyuubiFunSuite { diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/SingleSessionSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/SingleSessionSuite.scala index 0f0e07411a4..82a85bfcf44 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/SingleSessionSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/SingleSessionSuite.scala @@ -28,7 +28,7 @@ class SingleSessionSuite extends WithSparkSQLEngine with HiveJDBCTestHelper { ENGINE_SHARE_LEVEL.key -> "SERVER", ENGINE_SINGLE_SPARK_SESSION.key -> "true", ( - ENGINE_SESSION_INITIALIZE_SQL.key, + ENGINE_SESSION_SPARK_INITIALIZE_SQL.key, "CREATE DATABASE IF NOT EXISTS INIT_DB_SOLO;" + "CREATE TABLE IF NOT EXISTS INIT_DB_SOLO.test(a int) USING CSV;" + "INSERT INTO INIT_DB_SOLO.test VALUES (2);")) diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/UserIsolatedSessionSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/UserIsolatedSessionSuite.scala index 9d31e180f7e..ccfea6b89c4 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/UserIsolatedSessionSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/session/UserIsolatedSessionSuite.scala @@ -17,11 +17,10 @@ package org.apache.kyuubi.engine.spark.session -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TOpenSessionReq} - import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.engine.spark.WithSparkSQLEngine import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TOpenSessionReq} class UserIsolatedSessionSuite extends WithSparkSQLEngine with HiveJDBCTestHelper { diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/spark/kyuubi/SQLOperationListenerSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/spark/kyuubi/SQLOperationListenerSuite.scala index f732f7c3846..d6987ad2e67 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/spark/kyuubi/SQLOperationListenerSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/spark/kyuubi/SQLOperationListenerSuite.scala @@ -19,19 +19,17 @@ package org.apache.spark.kyuubi import scala.collection.JavaConverters.asScalaBufferConverter -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchOrientation, TFetchResultsReq, TOperationHandle} import org.scalatest.time.SpanSugar._ import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.OPERATION_SPARK_LISTENER_ENABLED import org.apache.kyuubi.engine.spark.WithSparkSQLEngine import org.apache.kyuubi.operation.HiveJDBCTestHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchOrientation, TFetchResultsReq, TOperationHandle} class SQLOperationListenerSuite extends WithSparkSQLEngine with HiveJDBCTestHelper { - override def withKyuubiConf: Map[String, String] = Map( - KyuubiConf.ENGINE_SPARK_SHOW_PROGRESS.key -> "true", - KyuubiConf.ENGINE_SPARK_SHOW_PROGRESS_UPDATE_INTERVAL.key -> "200") + override def withKyuubiConf: Map[String, String] = Map.empty override protected def jdbcUrl: String = getJdbcUrl @@ -58,19 +56,23 @@ class SQLOperationListenerSuite extends WithSparkSQLEngine with HiveJDBCTestHelp } test("operation listener with progress job info") { - val sql = "SELECT java_method('java.lang.Thread', 'sleep', 10000l) FROM range(1, 3, 1, 2);" - withSessionHandle { (client, handle) => - val req = new TExecuteStatementReq() - req.setSessionHandle(handle) - req.setStatement(sql) - val tExecuteStatementResp = client.ExecuteStatement(req) - val opHandle = tExecuteStatementResp.getOperationHandle - val fetchResultsReq = new TFetchResultsReq(opHandle, TFetchOrientation.FETCH_NEXT, 1000) - fetchResultsReq.setFetchType(1.toShort) - eventually(timeout(90.seconds), interval(500.milliseconds)) { - val resultsResp = client.FetchResults(fetchResultsReq) - val logs = resultsResp.getResults.getColumns.get(0).getStringVal.getValues.asScala - assert(logs.exists(_.matches(".*\\[Job .* Stages\\] \\[Stage .*\\]"))) + withSessionConf(Map( + KyuubiConf.ENGINE_SPARK_SHOW_PROGRESS.key -> "true", + KyuubiConf.ENGINE_SPARK_SHOW_PROGRESS_UPDATE_INTERVAL.key -> "200"))()() { + val sql = "SELECT java_method('java.lang.Thread', 'sleep', 10000l) FROM range(1, 3, 1, 2);" + withSessionHandle { (client, handle) => + val req = new TExecuteStatementReq() + req.setSessionHandle(handle) + req.setStatement(sql) + val tExecuteStatementResp = client.ExecuteStatement(req) + val opHandle = tExecuteStatementResp.getOperationHandle + val fetchResultsReq = new TFetchResultsReq(opHandle, TFetchOrientation.FETCH_NEXT, 1000) + fetchResultsReq.setFetchType(1.toShort) + eventually(timeout(90.seconds), interval(500.milliseconds)) { + val resultsResp = client.FetchResults(fetchResultsReq) + val logs = resultsResp.getResults.getColumns.get(0).getStringVal.getValues.asScala + assert(logs.exists(_.matches(".*\\[Job .* Stages\\] \\[Stage .*\\]"))) + } } } } diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/ExecuteStatement.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/ExecuteStatement.scala index 3e7cce80cdf..3de2ae59f42 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/ExecuteStatement.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/ExecuteStatement.scala @@ -19,17 +19,16 @@ package org.apache.kyuubi.engine.trino.operation import java.util.concurrent.RejectedExecutionException -import org.apache.hive.service.rpc.thrift.TFetchResultsResp - import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.engine.trino.TrinoStatement import org.apache.kyuubi.engine.trino.event.TrinoOperationEvent -import org.apache.kyuubi.engine.trino.schema.RowSet +import org.apache.kyuubi.engine.trino.schema.TrinoTRowSetGenerator import org.apache.kyuubi.events.EventBus import org.apache.kyuubi.operation.{ArrayFetchIterator, FetchIterator, OperationState} import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FETCH_PRIOR, FetchOrientation} import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TFetchResultsResp class ExecuteStatement( session: Session, @@ -97,7 +96,8 @@ class ExecuteStatement( throw KyuubiSQLException(s"Fetch orientation[$order] is not supported in $mode mode") } val taken = iter.take(rowSetSize) - val resultRowSet = RowSet.toTRowSet(taken.toList, schema, getProtocolVersion) + val resultRowSet = new TrinoTRowSetGenerator() + .toTRowSet(taken.toList, schema, getProtocolVersion) resultRowSet.setStartRowOffset(iter.getPosition) val fetchResultsResp = new TFetchResultsResp(OK_STATUS) fetchResultsResp.setResults(resultRowSet) diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperation.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperation.scala index 11eaa1bc1d7..822f1726a3b 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperation.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperation.scala @@ -21,13 +21,11 @@ import java.io.IOException import io.trino.client.Column import io.trino.client.StatementClient -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.Utils import org.apache.kyuubi.engine.trino.TrinoContext -import org.apache.kyuubi.engine.trino.schema.RowSet -import org.apache.kyuubi.engine.trino.schema.SchemaHelper +import org.apache.kyuubi.engine.trino.schema.{SchemaHelper, TrinoTRowSetGenerator} import org.apache.kyuubi.engine.trino.session.TrinoSessionImpl import org.apache.kyuubi.operation.AbstractOperation import org.apache.kyuubi.operation.FetchIterator @@ -35,6 +33,7 @@ import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FE import org.apache.kyuubi.operation.OperationState import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} abstract class TrinoOperation(session: Session) extends AbstractOperation(session) { @@ -66,7 +65,8 @@ abstract class TrinoOperation(session: Session) extends AbstractOperation(sessio case FETCH_FIRST => iter.fetchAbsolute(0) } val taken = iter.take(rowSetSize) - val resultRowSet = RowSet.toTRowSet(taken.toList, schema, getProtocolVersion) + val resultRowSet = + new TrinoTRowSetGenerator().toTRowSet(taken.toSeq, schema, getProtocolVersion) resultRowSet.setStartRowOffset(iter.getPosition) val resp = new TFetchResultsResp(OK_STATUS) resp.setResults(resultRowSet) diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/RowSet.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/RowSet.scala index 6e23a3e1f98..22e09f38138 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/RowSet.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/RowSet.scala @@ -17,233 +17,16 @@ package org.apache.kyuubi.engine.trino.schema -import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -import java.util import scala.collection.JavaConverters._ import io.trino.client.ClientStandardTypes._ import io.trino.client.ClientTypeSignature -import io.trino.client.Column import io.trino.client.Row -import org.apache.hive.service.rpc.thrift.TBinaryColumn -import org.apache.hive.service.rpc.thrift.TBoolColumn -import org.apache.hive.service.rpc.thrift.TBoolValue -import org.apache.hive.service.rpc.thrift.TByteColumn -import org.apache.hive.service.rpc.thrift.TByteValue -import org.apache.hive.service.rpc.thrift.TColumn -import org.apache.hive.service.rpc.thrift.TColumnValue -import org.apache.hive.service.rpc.thrift.TDoubleColumn -import org.apache.hive.service.rpc.thrift.TDoubleValue -import org.apache.hive.service.rpc.thrift.TI16Column -import org.apache.hive.service.rpc.thrift.TI16Value -import org.apache.hive.service.rpc.thrift.TI32Column -import org.apache.hive.service.rpc.thrift.TI32Value -import org.apache.hive.service.rpc.thrift.TI64Column -import org.apache.hive.service.rpc.thrift.TI64Value -import org.apache.hive.service.rpc.thrift.TProtocolVersion -import org.apache.hive.service.rpc.thrift.TRow -import org.apache.hive.service.rpc.thrift.TRowSet -import org.apache.hive.service.rpc.thrift.TStringColumn -import org.apache.hive.service.rpc.thrift.TStringValue - -import org.apache.kyuubi.util.RowSetUtils.bitSetToBuffer object RowSet { - def toTRowSet( - rows: Seq[List[_]], - schema: List[Column], - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBasedSet(rows, schema) - } else { - toColumnBasedSet(rows, schema) - } - } - - def toRowBasedSet(rows: Seq[List[_]], schema: List[Column]): TRowSet = { - val rowSize = rows.length - val tRows = new util.ArrayList[TRow](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - val columnSize = row.size - var j = 0 - while (j < columnSize) { - val columnValue = toTColumnValue(j, row, schema) - tRow.addToColVals(columnValue) - j += 1 - } - tRows.add(tRow) - i += 1 - } - new TRowSet(0, tRows) - } - - def toColumnBasedSet(rows: Seq[List[_]], schema: List[Column]): TRowSet = { - val size = rows.size - val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](size)) - val columnSize = schema.length - var i = 0 - while (i < columnSize) { - val field = schema(i) - val tColumn = toTColumn(rows, i, field.getTypeSignature) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - private def toTColumn( - rows: Seq[Seq[Any]], - ordinal: Int, - typ: ClientTypeSignature): TColumn = { - val nulls = new java.util.BitSet() - typ.getRawType match { - case BOOLEAN => - val values = getOrSetAsNull[java.lang.Boolean](rows, ordinal, nulls, true) - TColumn.boolVal(new TBoolColumn(values, nulls)) - - case TINYINT => - val values = getOrSetAsNull[java.lang.Byte](rows, ordinal, nulls, 0.toByte) - TColumn.byteVal(new TByteColumn(values, nulls)) - - case SMALLINT => - val values = getOrSetAsNull[java.lang.Short](rows, ordinal, nulls, 0.toShort) - TColumn.i16Val(new TI16Column(values, nulls)) - - case INTEGER => - val values = getOrSetAsNull[java.lang.Integer](rows, ordinal, nulls, 0) - TColumn.i32Val(new TI32Column(values, nulls)) - - case BIGINT => - val values = getOrSetAsNull[java.lang.Long](rows, ordinal, nulls, 0L) - TColumn.i64Val(new TI64Column(values, nulls)) - - case REAL => - val values = getOrSetAsNull[java.lang.Float](rows, ordinal, nulls, 0.toFloat) - .asScala.map(n => java.lang.Double.valueOf(n.toString)).asJava - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case DOUBLE => - val values = getOrSetAsNull[java.lang.Double](rows, ordinal, nulls, 0.toDouble) - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case VARCHAR => - val values = getOrSetAsNull[String](rows, ordinal, nulls, "") - TColumn.stringVal(new TStringColumn(values, nulls)) - - case VARBINARY => - val values = getOrSetAsNull[Array[Byte]](rows, ordinal, nulls, Array()) - .asScala - .map(ByteBuffer.wrap) - .asJava - TColumn.binaryVal(new TBinaryColumn(values, nulls)) - - case _ => - val rowSize = rows.length - val values = new util.ArrayList[String](rowSize) - var i = 0 - while (i < rowSize) { - val row = rows(i) - nulls.set(i, row(ordinal) == null) - val value = - if (row(ordinal) == null) { - "" - } else { - toHiveString(row(ordinal), typ) - } - values.add(value) - i += 1 - } - TColumn.stringVal(new TStringColumn(values, nulls)) - } - } - - private def getOrSetAsNull[T]( - rows: Seq[Seq[Any]], - ordinal: Int, - nulls: java.util.BitSet, - defaultVal: T): java.util.List[T] = { - val size = rows.length - val ret = new java.util.ArrayList[T](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row(ordinal) == null - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row(ordinal).asInstanceOf[T]) - } - idx += 1 - } - ret - } - - private def toTColumnValue( - ordinal: Int, - row: List[Any], - types: List[Column]): TColumnValue = { - - types(ordinal).getTypeSignature.getRawType match { - case BOOLEAN => - val boolValue = new TBoolValue - if (row(ordinal) != null) boolValue.setValue(row(ordinal).asInstanceOf[Boolean]) - TColumnValue.boolVal(boolValue) - - case TINYINT => - val byteValue = new TByteValue - if (row(ordinal) != null) byteValue.setValue(row(ordinal).asInstanceOf[Byte]) - TColumnValue.byteVal(byteValue) - - case SMALLINT => - val tI16Value = new TI16Value - if (row(ordinal) != null) tI16Value.setValue(row(ordinal).asInstanceOf[Short]) - TColumnValue.i16Val(tI16Value) - - case INTEGER => - val tI32Value = new TI32Value - if (row(ordinal) != null) tI32Value.setValue(row(ordinal).asInstanceOf[Int]) - TColumnValue.i32Val(tI32Value) - - case BIGINT => - val tI64Value = new TI64Value - if (row(ordinal) != null) tI64Value.setValue(row(ordinal).asInstanceOf[Long]) - TColumnValue.i64Val(tI64Value) - - case REAL => - val tDoubleValue = new TDoubleValue - if (row(ordinal) != null) { - val doubleValue = java.lang.Double.valueOf(row(ordinal).asInstanceOf[Float].toString) - tDoubleValue.setValue(doubleValue) - } - TColumnValue.doubleVal(tDoubleValue) - - case DOUBLE => - val tDoubleValue = new TDoubleValue - if (row(ordinal) != null) tDoubleValue.setValue(row(ordinal).asInstanceOf[Double]) - TColumnValue.doubleVal(tDoubleValue) - - case VARCHAR => - val tStringValue = new TStringValue - if (row(ordinal) != null) tStringValue.setValue(row(ordinal).asInstanceOf[String]) - TColumnValue.stringVal(tStringValue) - - case _ => - val tStrValue = new TStringValue - if (row(ordinal) != null) { - tStrValue.setValue( - toHiveString(row(ordinal), types(ordinal).getTypeSignature)) - } - TColumnValue.stringVal(tStrValue) - } - } - /** * A simpler impl of Trino's toHiveString */ diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelper.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelper.scala index e89f5e8cd97..ad44445c30a 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelper.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelper.scala @@ -24,15 +24,16 @@ import scala.collection.JavaConverters._ import io.trino.client.ClientStandardTypes._ import io.trino.client.ClientTypeSignature import io.trino.client.Column -import org.apache.hive.service.rpc.thrift.TCLIServiceConstants -import org.apache.hive.service.rpc.thrift.TColumnDesc -import org.apache.hive.service.rpc.thrift.TPrimitiveTypeEntry -import org.apache.hive.service.rpc.thrift.TTableSchema -import org.apache.hive.service.rpc.thrift.TTypeDesc -import org.apache.hive.service.rpc.thrift.TTypeEntry -import org.apache.hive.service.rpc.thrift.TTypeId -import org.apache.hive.service.rpc.thrift.TTypeQualifiers -import org.apache.hive.service.rpc.thrift.TTypeQualifierValue + +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIServiceConstants +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TColumnDesc +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TPrimitiveTypeEntry +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTableSchema +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeDesc +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeEntry +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeQualifiers +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeQualifierValue object SchemaHelper { diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/TrinoTRowSetGenerator.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/TrinoTRowSetGenerator.scala new file mode 100644 index 00000000000..57d91b371f0 --- /dev/null +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/schema/TrinoTRowSetGenerator.scala @@ -0,0 +1,76 @@ +/* + * 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.kyuubi.engine.trino.schema + +import io.trino.client.{ClientTypeSignature, Column} +import io.trino.client.ClientStandardTypes._ + +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.engine.trino.schema.RowSet.toHiveString +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +class TrinoTRowSetGenerator + extends TRowSetGenerator[Seq[Column], Seq[_], ClientTypeSignature] { + + override def getColumnSizeFromSchemaType(schema: Seq[Column]): Int = schema.length + + override def getColumnType(schema: Seq[Column], ordinal: Int): ClientTypeSignature = + schema(ordinal).getTypeSignature + + override def isColumnNullAt(row: Seq[_], ordinal: Int): Boolean = row(ordinal) == null + + override def getColumnAs[T](row: Seq[_], ordinal: Int): T = row(ordinal).asInstanceOf[T] + + override def toTColumn(rows: Seq[Seq[_]], ordinal: Int, typ: ClientTypeSignature): TColumn = { + typ.getRawType match { + case BOOLEAN => asBooleanTColumn(rows, ordinal) + case TINYINT => asByteTColumn(rows, ordinal) + case SMALLINT => asShortTColumn(rows, ordinal) + case INTEGER => asIntegerTColumn(rows, ordinal) + case BIGINT => asLongTColumn(rows, ordinal) + case REAL => asFloatTColumn(rows, ordinal) + case DOUBLE => asDoubleTColumn(rows, ordinal) + case VARCHAR => asStringTColumn(rows, ordinal) + case VARBINARY => asByteArrayTColumn(rows, ordinal) + case _ => + asStringTColumn( + rows, + ordinal, + convertFunc = (row, ordinal) => toHiveString(getColumnAs[Any](row, ordinal), typ)) + } + } + + override def toTColumnValue(row: Seq[_], ordinal: Int, types: Seq[Column]): TColumnValue = { + getColumnType(types, ordinal).getRawType match { + case BOOLEAN => asBooleanTColumnValue(row, ordinal) + case TINYINT => asByteTColumnValue(row, ordinal) + case SMALLINT => asShortTColumnValue(row, ordinal) + case INTEGER => asIntegerTColumnValue(row, ordinal) + case BIGINT => asLongTColumnValue(row, ordinal) + case REAL => asFloatTColumnValue(row, ordinal) + case DOUBLE => asDoubleTColumnValue(row, ordinal) + case VARCHAR => asStringTColumnValue(row, ordinal) + case _ => + asStringTColumnValue( + row, + ordinal, + rawValue => toHiveString(rawValue, types(ordinal).getTypeSignature)) + } + } + +} diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionImpl.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionImpl.scala index 0b3ac01a9ef..950a0814b5d 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionImpl.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionImpl.scala @@ -28,7 +28,6 @@ import io.airlift.units.Duration import io.trino.client.ClientSession import io.trino.client.OkHttpUtil import okhttp3.OkHttpClient -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.Utils.currentUser @@ -39,6 +38,7 @@ import org.apache.kyuubi.engine.trino.event.TrinoSessionEvent import org.apache.kyuubi.events.EventBus import org.apache.kyuubi.operation.{Operation, OperationHandle} import org.apache.kyuubi.session.{AbstractSession, SessionHandle, SessionManager, USE_CATALOG, USE_DATABASE} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TGetInfoValue, TProtocolVersion} class TrinoSessionImpl( protocol: TProtocolVersion, diff --git a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionManager.scala b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionManager.scala index e18b8f75817..55aa2f3fa78 100644 --- a/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionManager.scala +++ b/externals/kyuubi-trino-engine/src/main/scala/org/apache/kyuubi/engine/trino/session/TrinoSessionManager.scala @@ -17,14 +17,13 @@ package org.apache.kyuubi.engine.trino.session -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.config.KyuubiConf.ENGINE_SHARE_LEVEL import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY import org.apache.kyuubi.engine.ShareLevel import org.apache.kyuubi.engine.trino.TrinoSqlEngine import org.apache.kyuubi.engine.trino.operation.TrinoOperationManager import org.apache.kyuubi.session.{Session, SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class TrinoSessionManager extends SessionManager("TrinoSessionManager") { diff --git a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperationSuite.scala b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperationSuite.scala index 90939a3e4e0..c49c4965bfc 100644 --- a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperationSuite.scala +++ b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/operation/TrinoOperationSuite.scala @@ -22,13 +22,13 @@ import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.Set import io.trino.client.ClientStandardTypes._ -import org.apache.hive.service.rpc.thrift._ import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.engine.trino.{TrinoQueryTests, TrinoStatement, WithTrinoEngine} import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ class TrinoOperationSuite extends WithTrinoEngine with TrinoQueryTests { override def withKyuubiConf: Map[String, String] = Map( diff --git a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/RowSetSuite.scala b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/RowSetSuite.scala index d6187bbf881..461c453ecd2 100644 --- a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/RowSetSuite.scala +++ b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/RowSetSuite.scala @@ -28,11 +28,11 @@ import io.trino.client.ClientStandardTypes._ import io.trino.client.ClientTypeSignature import io.trino.client.Column import io.trino.client.Row -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.KyuubiFunSuite import org.apache.kyuubi.engine.trino.schema.RowSet.toHiveString import org.apache.kyuubi.engine.trino.util.TestUtils._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class RowSetSuite extends KyuubiFunSuite { @@ -126,7 +126,7 @@ class RowSetSuite extends KyuubiFunSuite { def uuidSuffix(value: Int): String = if (value > 9) value.toString else s"f$value" test("column based set") { - val tRowSet = RowSet.toColumnBasedSet(rows, schema) + val tRowSet = new TrinoTRowSetGenerator().toColumnBasedSet(rows, schema) assert(tRowSet.getColumns.size() === schema.size) assert(tRowSet.getRowsSize === 0) @@ -277,7 +277,7 @@ class RowSetSuite extends KyuubiFunSuite { } test("row based set") { - val tRowSet = RowSet.toRowBasedSet(rows, schema) + val tRowSet = new TrinoTRowSetGenerator().toRowBasedSet(rows, schema) assert(tRowSet.getColumnCount === 0) assert(tRowSet.getRowsSize === rows.size) val iter = tRowSet.getRowsIterator @@ -333,7 +333,7 @@ class RowSetSuite extends KyuubiFunSuite { test("to row set") { TProtocolVersion.values().foreach { proto => - val set = RowSet.toTRowSet(rows, schema, proto) + val set = new TrinoTRowSetGenerator().toTRowSet(rows, schema, proto) if (proto.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { assert(!set.isSetColumns, proto.toString) assert(set.isSetRows, proto.toString) diff --git a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelperSuite.scala b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelperSuite.scala index 6f6bdc25fa4..451cc0573f8 100644 --- a/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelperSuite.scala +++ b/externals/kyuubi-trino-engine/src/test/scala/org/apache/kyuubi/engine/trino/schema/SchemaHelperSuite.scala @@ -21,12 +21,12 @@ import scala.collection.JavaConverters._ import io.trino.client.ClientStandardTypes._ import io.trino.client.Column -import org.apache.hive.service.rpc.thrift.TCLIServiceConstants -import org.apache.hive.service.rpc.thrift.TTypeId import org.apache.kyuubi.KyuubiFunSuite import org.apache.kyuubi.engine.trino.schema.SchemaHelper._ import org.apache.kyuubi.engine.trino.util.TestUtils._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIServiceConstants +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId class SchemaHelperSuite extends KyuubiFunSuite { diff --git a/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuite.scala b/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuite.scala index 55476bfd003..8bd6ecf99ee 100644 --- a/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuite.scala +++ b/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuite.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.it.flink.operation -import org.apache.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.it.flink.WithKyuubiServerAndFlinkMiniCluster import org.apache.kyuubi.operation.HiveJDBCTestHelper import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant.TABLE_CAT +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} class FlinkOperationSuite extends WithKyuubiServerAndFlinkMiniCluster with HiveJDBCTestHelper { @@ -98,6 +97,8 @@ class FlinkOperationSuite extends WithKyuubiServerAndFlinkMiniCluster req.setSessionHandle(handle) req.setInfoType(TGetInfoType.CLI_DBMS_NAME) assert(client.GetInfo(req).getInfoValue.getStringValue === "Apache Flink") + req.setInfoType(TGetInfoType.CLI_ODBC_KEYWORDS) + assert(client.GetInfo(req).getInfoValue.getStringValue === "Unimplemented") } } } diff --git a/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuiteOnYarn.scala b/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuiteOnYarn.scala index ee6b9bb98ea..c48d91435f1 100644 --- a/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuiteOnYarn.scala +++ b/integration-tests/kyuubi-flink-it/src/test/scala/org/apache/kyuubi/it/flink/operation/FlinkOperationSuiteOnYarn.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.it.flink.operation -import org.apache.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.it.flink.WithKyuubiServerAndYarnMiniCluster import org.apache.kyuubi.operation.HiveJDBCTestHelper import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant.TABLE_CAT +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} class FlinkOperationSuiteOnYarn extends WithKyuubiServerAndYarnMiniCluster with HiveJDBCTestHelper { diff --git a/integration-tests/kyuubi-gluten-it/pom.xml b/integration-tests/kyuubi-gluten-it/pom.xml new file mode 100644 index 00000000000..ac49c286ade --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/pom.xml @@ -0,0 +1,127 @@ + + + + 4.0.0 + + org.apache.kyuubi + integration-tests + 1.9.0-SNAPSHOT + ../pom.xml + + + kyuubi-gluten-it_${scala.binary.version} + Kyuubi Test Gluten IT + https://kyuubi.apache.org/ + + + 1.1.0-SNAPSHOT + 3.4.2 + 3.4 + + + + + org.apache.kyuubi + kyuubi-common_${scala.binary.version} + ${project.version} + + + + org.apache.kyuubi + kyuubi-common_${scala.binary.version} + ${project.version} + test-jar + test + + + + org.apache.kyuubi + kyuubi-spark-connector-common_${scala.binary.version} + ${project.version} + test-jar + test + + + + org.apache.kyuubi + kyuubi-spark-connector-tpcds_${scala.binary.version} + ${project.version} + + + + org.apache.kyuubi + kyuubi-spark-connector-tpch_${scala.binary.version} + ${project.version} + + + + org.apache.spark + spark-sql_${scala.binary.version} + provided + + + + org.apache.spark + spark-hive_${scala.binary.version} + test + + + + + target/scala-${scala.binary.version}/classes + target/scala-${scala.binary.version}/test-classes + + + + + gluten-spark-3.4 + + org.apache.kyuubi.tags.GlutenTest + 3.4.2 + 3.4 + + + + io.glutenproject + gluten-velox-bundle-spark3.4_2.12-ubuntu_22.04 + ${gluten.version} + system + ${project.basedir}/../../gluten/package/target/gluten-velox-bundle-spark3.4_2.12-ubuntu_22.04-${gluten.version}.jar + + + + + gluten-spark-3.3 + + org.apache.kyuubi.tags.GlutenTest + 3.3.1 + 3.3 + + + + io.glutenproject + gluten-velox-bundle-spark3.3_2.12-ubuntu_22.04 + ${gluten.version} + system + ${project.basedir}/../../gluten/package/target/gluten-velox-bundle-spark3.3_2.12-ubuntu_22.04-${gluten.version}.jar + + + + + diff --git a/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpcds-tiny.sql b/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpcds-tiny.sql new file mode 100644 index 00000000000..952a9cf3a37 --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpcds-tiny.sql @@ -0,0 +1,146 @@ +-- 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. +-- + +CREATE DATABASE IF NOT EXISTS spark_catalog.tpcds_tiny; + +USE spark_catalog.tpcds_tiny; + +-- +-- Name: catalog_sales; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS catalog_sales USING parquet PARTITIONED BY (cs_sold_date_sk) +AS SELECT * FROM tpcds.tiny.catalog_sales; + +-- +-- Name: catalog_returns; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS catalog_returns USING parquet PARTITIONED BY (cr_returned_date_sk) +AS SELECT * FROM tpcds.tiny.catalog_returns; + +-- +-- Name: inventory; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS inventory USING parquet PARTITIONED BY (inv_date_sk) +AS SELECT * FROM tpcds.tiny.inventory; + +-- +-- Name: store_sales; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS store_sales USING parquet PARTITIONED BY (ss_sold_date_sk) +AS SELECT * FROM tpcds.tiny.store_sales; + +-- +-- Name: store_returns; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS store_returns USING parquet PARTITIONED BY (sr_returned_date_sk) +AS SELECT * FROM tpcds.tiny.store_returns; + +-- +-- Name: web_sales; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS web_sales USING parquet PARTITIONED BY (ws_sold_date_sk) +AS SELECT * FROM tpcds.tiny.web_sales; + +-- +-- Name: web_returns; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS web_returns USING parquet PARTITIONED BY (wr_returned_date_sk) +AS SELECT * FROM tpcds.tiny.web_returns; + +-- +-- Name: call_center; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS call_center USING parquet AS SELECT * FROM tpcds.tiny.call_center; + +-- +-- Name: catalog_page; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS catalog_page USING parquet AS SELECT * FROM tpcds.tiny.catalog_page; + +-- +-- Name: customer; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS customer USING parquet AS SELECT * FROM tpcds.tiny.customer; + +-- +-- Name: customer_address; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS customer_address USING parquet AS SELECT * FROM tpcds.tiny.customer_address; + +-- +-- Name: customer_demographics; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS customer_demographics USING parquet AS SELECT * FROM tpcds.tiny.customer_demographics; + +-- +-- Name: date_dim; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS date_dim USING parquet AS SELECT * FROM tpcds.tiny.date_dim; + +-- +-- Name: household_demographics; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS household_demographics USING parquet AS SELECT * FROM tpcds.tiny.household_demographics; + +-- +-- Name: income_band; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS income_band USING parquet AS SELECT * FROM tpcds.tiny.income_band; + +-- +-- Name: item; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS item USING parquet AS SELECT * FROM tpcds.tiny.item; + +-- +-- Name: promotion; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS promotion USING parquet AS SELECT * FROM tpcds.tiny.promotion; + +-- +-- Name: reason; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS reason USING parquet AS SELECT * FROM tpcds.tiny.reason; + +-- +-- Name: ship_mode; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS ship_mode USING parquet AS SELECT * FROM tpcds.tiny.ship_mode; + +-- +-- Name: store; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS store USING parquet AS SELECT * FROM tpcds.tiny.store; + +-- +-- Name: time_dim; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS time_dim USING parquet AS SELECT * FROM tpcds.tiny.time_dim; + +-- +-- Name: warehouse; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS warehouse USING parquet AS SELECT * FROM tpcds.tiny.warehouse; + +-- +-- Name: web_page; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS web_page USING parquet AS SELECT * FROM tpcds.tiny.web_page; + +-- +-- Name: web_site; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS web_site USING parquet AS SELECT * FROM tpcds.tiny.web_site; diff --git a/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpch-tiny.sql b/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpch-tiny.sql new file mode 100644 index 00000000000..8f2228f549c --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/resources/load-tpch-tiny.sql @@ -0,0 +1,59 @@ +-- 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. +-- + +CREATE DATABASE IF NOT EXISTS spark_catalog.tpch_tiny; + +USE spark_catalog.tpch_tiny; + +-- +-- Name: customer; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS customer USING parquet AS SELECT * FROM tpch.tiny.customer; + +-- +-- Name: orders; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS orders USING parquet AS SELECT * FROM tpch.tiny.orders; + +-- +-- Name: lineitem; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS lineitem USING parquet AS SELECT * FROM tpch.tiny.lineitem; + +-- +-- Name: part; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS part USING parquet AS SELECT * FROM tpch.tiny.part; + +-- +-- Name: partsupp; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS partsupp USING parquet AS SELECT * FROM tpch.tiny.partsupp; + +-- +-- Name: supplier; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS supplier USING parquet AS SELECT * FROM tpch.tiny.supplier; + +-- +-- Name: nation; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS nation USING parquet AS SELECT * FROM tpch.tiny.nation; + +-- +-- Name: region; Type: TABLE; Tablespace: +-- +CREATE TABLE IF NOT EXISTS region USING parquet AS SELECT * FROM tpch.tiny.region; diff --git a/integration-tests/kyuubi-gluten-it/src/test/resources/log4j2-test.xml b/integration-tests/kyuubi-gluten-it/src/test/resources/log4j2-test.xml new file mode 100644 index 00000000000..3110216c17c --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/resources/log4j2-test.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/GlutenSuite.scala b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/GlutenSuite.scala new file mode 100644 index 00000000000..67e9a92b66b --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/GlutenSuite.scala @@ -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. + */ + +package org.apache.kyuubi.it.gluten + +import org.apache.spark.SparkConf +import org.apache.spark.sql.SparkSession + +import org.apache.kyuubi.{GlutenSuiteMixin, KyuubiFunSuite} +import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSession +import org.apache.kyuubi.tags.GlutenTest + +@GlutenTest +class GlutenSuite extends KyuubiFunSuite with GlutenSuiteMixin { + + lazy val sparkConf: SparkConf = { + val glutenConf = new SparkConf().setMaster("local[*]") + .set("spark.ui.enabled", "false") + extraConfigs.foreach { case (k, v) => glutenConf.set(k, v) } + glutenConf + } + + test("KYUUBI #5467:test gluten select") { + withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => + val result = spark.sql("SELECT 1").head() + assert(result.get(0) == 1) + } + } + + test("KYUUBI #5467: test gluten plan") { + withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => + val plan = spark.sql("explain SELECT 1").head().getString(0) + assert(plan.contains("VeloxColumnarToRowExec") && plan.contains( + "VeloxColumnarToRowExec") && plan.contains("RowToVeloxColumnar")) + } + } +} diff --git a/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/TPCUtils.scala b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/TPCUtils.scala new file mode 100644 index 00000000000..667a237809e --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/TPCUtils.scala @@ -0,0 +1,32 @@ +/* + * 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.kyuubi.it.gluten + +import scala.io.{Codec, Source} + +import org.apache.kyuubi.Utils + +object TPCUtils { + def loadTPCFile(resourceFile: String): String = { + val in = Utils.getContextOrKyuubiClassLoader + .getResourceAsStream(resourceFile) + val str: String = Source.fromInputStream(in)(Codec.UTF8).mkString + in.close() + str + } +} diff --git a/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpcds/GlutenTPCDSQuerySuite.scala b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpcds/GlutenTPCDSQuerySuite.scala new file mode 100644 index 00000000000..9110974a323 --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpcds/GlutenTPCDSQuerySuite.scala @@ -0,0 +1,86 @@ +/* + * 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.kyuubi.it.gluten.tpcds + +import scala.collection.JavaConverters._ + +import org.apache.spark.SparkConf +import org.apache.spark.sql.SparkSession +import org.scalatest.tags.Slow + +import org.apache.kyuubi.{GlutenSuiteMixin, KyuubiFunSuite} +import org.apache.kyuubi.it.gluten.TPCUtils.loadTPCFile +import org.apache.kyuubi.spark.connector.common.GoldenFileUtils.LICENSE_HEADER +import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSession +import org.apache.kyuubi.spark.connector.tpcds.TPCDSCatalog +import org.apache.kyuubi.tags.GlutenTest + +@Slow +@GlutenTest +class GlutenTPCDSQuerySuite extends KyuubiFunSuite with GlutenSuiteMixin { + + val queries: Set[String] = (1 to 99).map(i => s"q$i").toSet - + ("q14", "q23", "q24", "q39") + + ("q14a", "q14b", "q23a", "q23b", "q24a", "q24b", "q39a", "q39b") - + // TODO:Fix gluten tpc-ds query test + ("q1", "q4", "q7", "q11", "q12", "q17", "q20", "q21", "q25", "q26", "q29", "q30", "q34", "q37", + "q39a", "q39b", "q40", "q43", "q46", "q49", "q56", "q58", "q59", "q60", "q68", "q73", "q74", + "q78", "q79", "q81", "q82", "q83", "q84", "q91", "q98") + lazy val sparkConf: SparkConf = { + val glutenConf = new SparkConf().setMaster("local[*]") + .set("spark.ui.enabled", "false") + .set("spark.sql.catalogImplementation", "in-memory") + .set("spark.sql.catalog.tpcds", classOf[TPCDSCatalog].getName) + .set("spark.sql.catalog.tpcds.useTableSchema_2_6", "true") + extraConfigs.foreach { case (k, v) => glutenConf.set(k, v) } + glutenConf + } + + test("KYUUBI #5467:gluten tpc-ds tiny query suite") { + val viewSuffix = "view" + withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => + loadTPDSTINY(spark) + queries.map { queryName => + queryName -> loadTPCFile(s"kyuubi/tpcds_3.2/$queryName.sql") + }.foreach { case (name, sql) => + try { + val result = spark.sql(sql).collect() + val schema = spark.sql(sql).schema + val schemaDDL = LICENSE_HEADER + schema.toDDL + "\n" + spark.createDataFrame(result.toList.asJava, schema).createTempView(s"$name$viewSuffix") + val sumHashResult = LICENSE_HEADER + spark.sql( + s"select sum(hash(*)) from $name$viewSuffix").collect().head.get(0) + "\n" + val expectHash = loadTPCFile(s"kyuubi/tpcds_3.2/$name.output.hash") + val expectSchema = loadTPCFile(s"kyuubi/tpcds_3.2/$name.output.schema") + assert(schemaDDL == expectSchema) + assert(sumHashResult == expectHash) + } catch { + case cause: Throwable => + fail(name, cause) + } + } + } + } + + def loadTPDSTINY(sc: SparkSession): Unit = { + val queryContent: String = loadTPCFile("load-tpcds-tiny.sql") + queryContent.split(";\n").filterNot(_.trim.isEmpty).foreach { sql => + sc.sql(sql) + } + } +} diff --git a/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpch/GlutenTPCHQuerySuite.scala b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpch/GlutenTPCHQuerySuite.scala new file mode 100644 index 00000000000..98b4e94489d --- /dev/null +++ b/integration-tests/kyuubi-gluten-it/src/test/scala/org/apache/kyuubi/it/gluten/tpch/GlutenTPCHQuerySuite.scala @@ -0,0 +1,75 @@ +/* + * 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.kyuubi.it.gluten.tpch + +import scala.collection.JavaConverters._ + +import org.apache.spark.SparkConf +import org.apache.spark.sql.SparkSession +import org.scalatest.tags.Slow + +import org.apache.kyuubi.{GlutenSuiteMixin, KyuubiFunSuite} +import org.apache.kyuubi.it.gluten.TPCUtils.loadTPCFile +import org.apache.kyuubi.spark.connector.common.GoldenFileUtils.LICENSE_HEADER +import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSession +import org.apache.kyuubi.spark.connector.tpch.TPCHCatalog +import org.apache.kyuubi.tags.GlutenTest + +@Slow +@GlutenTest +class GlutenTPCHQuerySuite extends KyuubiFunSuite with GlutenSuiteMixin { + // TODO: Fix the inconsistency in q9 results. + val queries: Set[String] = (1 to 22).map(i => s"q$i").toSet - "q9" + + lazy val sparkConf: SparkConf = { + val glutenConf = new SparkConf().setMaster("local[*]") + .set("spark.ui.enabled", "false") + .set("spark.sql.catalogImplementation", "in-memory") + .set("spark.sql.catalog.tpch", classOf[TPCHCatalog].getName) + extraConfigs.foreach { case (k, v) => glutenConf.set(k, v) } + glutenConf + } + + test("KYUUBI #5467:gluten tpc-h tiny query suite") { + val viewSuffix = "view" + withSparkSession(SparkSession.builder.config(sparkConf).getOrCreate()) { spark => + loadTPCHTINY(spark) + queries.map { queryName => + queryName -> loadTPCFile(s"kyuubi/tpch/$queryName.sql") + }.foreach { case (name, sql) => + val result = spark.sql(sql).collect() + val schema = spark.sql(sql).schema + val schemaDDL = LICENSE_HEADER + schema.toDDL + "\n" + spark.createDataFrame(result.toList.asJava, schema).createTempView(s"$name$viewSuffix") + val sumHashResult = LICENSE_HEADER + spark.sql( + s"select sum(hash(*)) from $name$viewSuffix").collect().head.get(0) + "\n" + val expectHash = loadTPCFile(s"kyuubi/tpch/$name.output.hash") + val expectSchema = loadTPCFile(s"kyuubi/tpch/$name.output.schema") + assert(schemaDDL == expectSchema, s"query $name schema not match") + assert(sumHashResult == expectHash, s"query $name result not match") + } + } + } + + def loadTPCHTINY(sc: SparkSession): Unit = { + val queryContent: String = loadTPCFile("load-tpch-tiny.sql") + queryContent.split(";\n").filterNot(_.trim.isEmpty).foreach { sql => + sc.sql(sql) + } + } +} diff --git a/integration-tests/kyuubi-hive-it/src/test/scala/org/apache/kyuubi/it/hive/operation/KyuubiOperationHiveEnginePerUserSuite.scala b/integration-tests/kyuubi-hive-it/src/test/scala/org/apache/kyuubi/it/hive/operation/KyuubiOperationHiveEnginePerUserSuite.scala index 07e2bc0f2c7..fd9e76bc3d4 100644 --- a/integration-tests/kyuubi-hive-it/src/test/scala/org/apache/kyuubi/it/hive/operation/KyuubiOperationHiveEnginePerUserSuite.scala +++ b/integration-tests/kyuubi-hive-it/src/test/scala/org/apache/kyuubi/it/hive/operation/KyuubiOperationHiveEnginePerUserSuite.scala @@ -17,11 +17,10 @@ package org.apache.kyuubi.it.hive.operation -import org.apache.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} - import org.apache.kyuubi.{HiveEngineTests, Utils, WithKyuubiServer} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoReq, TGetInfoType} class KyuubiOperationHiveEnginePerUserSuite extends WithKyuubiServer with HiveEngineTests { @@ -62,6 +61,20 @@ class KyuubiOperationHiveEnginePerUserSuite extends WithKyuubiServer with HiveEn } } + test("[KYUUBI #5865] Hive engine CLI_ODBC_KEYWORDS") { + withSessionConf(Map(KyuubiConf.SERVER_INFO_PROVIDER.key -> "ENGINE"))()() { + withSessionHandle { (client, handle) => + val req = new TGetInfoReq() + req.setSessionHandle(handle) + req.setInfoType(TGetInfoType.CLI_ODBC_KEYWORDS) + val value = client.GetInfo(req).getInfoValue.getStringValue + assert(value.contains("DATABASE") || value === "Unimplemented") + // excluded keywords + assert(!value.contains("ADD")) + } + } + } + test("kyuubi defined function - system_user, session_user") { withJdbcStatement("hive_engine_test") { statement => val rs = statement.executeQuery("SELECT system_user(), session_user()") diff --git a/integration-tests/kyuubi-jdbc-it/pom.xml b/integration-tests/kyuubi-jdbc-it/pom.xml index 95ffd2038c1..7921d94e217 100644 --- a/integration-tests/kyuubi-jdbc-it/pom.xml +++ b/integration-tests/kyuubi-jdbc-it/pom.xml @@ -78,6 +78,24 @@ testcontainers-scala-scalatest_${scala.binary.version} test + + + com.dimafeng + testcontainers-scala-mysql_${scala.binary.version} + test + + + + com.mysql + mysql-connector-j + test + + + + com.dimafeng + testcontainers-scala-postgresql_${scala.binary.version} + test + @@ -108,6 +126,13 @@ true ${project.build.directory} + + org.postgresql + postgresql + ${postgresql.version} + true + ${project.build.directory} + diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/OperationWithServerSuite.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/OperationWithServerSuite.scala new file mode 100644 index 00000000000..263de3d1528 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/OperationWithServerSuite.scala @@ -0,0 +1,27 @@ +/* + * 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.kyuubi.it.jdbc.mysql + +import org.apache.kyuubi.engine.jdbc.mysql.MySQLOperationSuite + +class OperationWithServerSuite extends MySQLOperationSuite + with WithKyuubiServerAndMySQLContainer { + + override protected def jdbcUrl: String = getJdbcUrl + +} diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/WithKyuubiServerAndMySQLContainer.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/WithKyuubiServerAndMySQLContainer.scala new file mode 100644 index 00000000000..da94df8e799 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/mysql/WithKyuubiServerAndMySQLContainer.scala @@ -0,0 +1,59 @@ +/* + * 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.kyuubi.it.jdbc.mysql + +import java.nio.file.{Files, Path, Paths} + +import org.apache.kyuubi.{Utils, WithKyuubiServer} +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_JDBC_EXTRA_CLASSPATH, KYUUBI_ENGINE_ENV_PREFIX, KYUUBI_HOME} +import org.apache.kyuubi.engine.jdbc.mysql.WithMySQLEngine + +trait WithKyuubiServerAndMySQLContainer extends WithKyuubiServer with WithMySQLEngine { + + private val kyuubiHome: String = Utils + .getCodeSourceLocation(getClass).split("integration-tests").head + + private val mysqlJdbcConnectorPath: String = { + val keyword = "mysql-connector" + + val jarsDir = Paths.get(kyuubiHome) + .resolve("integration-tests") + .resolve("kyuubi-jdbc-it") + .resolve("target") + + Files.list(jarsDir) + .filter { p: Path => p.getFileName.toString contains keyword } + .findFirst + .orElseThrow { () => new IllegalStateException(s"Can not find $keyword in $jarsDir.") } + .toAbsolutePath + .toString + } + + override protected val conf: KyuubiConf = { + KyuubiConf() + .set(s"$KYUUBI_ENGINE_ENV_PREFIX.$KYUUBI_HOME", kyuubiHome) + .set(ENGINE_JDBC_EXTRA_CLASSPATH, mysqlJdbcConnectorPath) + } + + override def beforeAll(): Unit = { + val configs = withKyuubiConf + configs.foreach(config => conf.set(config._1, config._2)) + super.beforeAll() + } +} diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/OperationWithServerSuite.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/OperationWithServerSuite.scala new file mode 100644 index 00000000000..41c31d38585 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/OperationWithServerSuite.scala @@ -0,0 +1,27 @@ +/* + * 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.kyuubi.it.jdbc.postgresql + +import org.apache.kyuubi.engine.jdbc.postgresql.PostgreSQLOperationSuite + +class OperationWithServerSuite extends PostgreSQLOperationSuite + with WithKyuubiServerAndPostgreSQLContainer { + + override protected def jdbcUrl: String = getJdbcUrl + +} diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/SessionWithServerSuite.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/SessionWithServerSuite.scala new file mode 100644 index 00000000000..79f34c79a47 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/SessionWithServerSuite.scala @@ -0,0 +1,27 @@ +/* + * 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.kyuubi.it.jdbc.postgresql + +import org.apache.kyuubi.engine.jdbc.postgresql.SessionSuite + +class SessionWithServerSuite extends SessionSuite + with WithKyuubiServerAndPostgreSQLContainer { + + override protected def jdbcUrl: String = getJdbcUrl + +} diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/StatementWithServerSuite.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/StatementWithServerSuite.scala new file mode 100644 index 00000000000..1c309371f74 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/StatementWithServerSuite.scala @@ -0,0 +1,27 @@ +/* + * 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.kyuubi.it.jdbc.postgresql + +import org.apache.kyuubi.engine.jdbc.postgresql.StatementSuite + +class StatementWithServerSuite extends StatementSuite + with WithKyuubiServerAndPostgreSQLContainer { + + override protected def jdbcUrl: String = getJdbcUrl + +} diff --git a/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/WithKyuubiServerAndPostgreSQLContainer.scala b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/WithKyuubiServerAndPostgreSQLContainer.scala new file mode 100644 index 00000000000..2e75d516a61 --- /dev/null +++ b/integration-tests/kyuubi-jdbc-it/src/test/scala/org/apache/kyuubi/it/jdbc/postgresql/WithKyuubiServerAndPostgreSQLContainer.scala @@ -0,0 +1,60 @@ +/* + * 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.kyuubi.it.jdbc.postgresql + +import java.nio.file.{Files, Path, Paths} +import java.time.Duration + +import org.apache.kyuubi.{Utils, WithKyuubiServer} +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_IDLE_TIMEOUT, ENGINE_JDBC_EXTRA_CLASSPATH, KYUUBI_ENGINE_ENV_PREFIX, KYUUBI_HOME} +import org.apache.kyuubi.engine.jdbc.postgresql.WithPostgreSQLEngine + +trait WithKyuubiServerAndPostgreSQLContainer extends WithKyuubiServer with WithPostgreSQLEngine { + + private val kyuubiHome: String = Utils + .getCodeSourceLocation(getClass).split("integration-tests").head + + private val postgresqlJdbcConnectorPath: String = { + val keyword = "postgresql" + + val jarsDir = Paths.get(kyuubiHome) + .resolve("integration-tests") + .resolve("kyuubi-jdbc-it") + .resolve("target") + + Files.list(jarsDir) + .filter { p: Path => p.getFileName.toString contains keyword } + .findFirst + .orElseThrow { () => new IllegalStateException(s"Can not find $keyword in $jarsDir.") } + .toAbsolutePath + .toString + } + + override protected val conf: KyuubiConf = { + KyuubiConf() + .set(s"$KYUUBI_ENGINE_ENV_PREFIX.$KYUUBI_HOME", kyuubiHome) + .set(ENGINE_JDBC_EXTRA_CLASSPATH, postgresqlJdbcConnectorPath) + .set(ENGINE_IDLE_TIMEOUT, Duration.ofMinutes(1).toMillis) + } + + override def beforeAll(): Unit = { + val configs = withKyuubiConf + configs.foreach(config => conf.set(config._1, config._2)) + super.beforeAll() + } +} diff --git a/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/deployment/KyuubiOnKubernetesTestsSuite.scala b/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/deployment/KyuubiOnKubernetesTestsSuite.scala index 95e15e6ebdd..9d47ab99815 100644 --- a/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/deployment/KyuubiOnKubernetesTestsSuite.scala +++ b/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/deployment/KyuubiOnKubernetesTestsSuite.scala @@ -55,7 +55,7 @@ class KyuubiOnKubernetesWithSparkTestsBase extends WithKyuubiServerOnKubernetes Map( "spark.master" -> s"k8s://$miniKubeApiMaster", // We should update spark docker image in ./github/workflows/master.yml at the same time - "spark.kubernetes.container.image" -> "apache/spark:3.4.1", + "spark.kubernetes.container.image" -> "apache/spark:3.4.2", "spark.kubernetes.container.image.pullPolicy" -> "IfNotPresent", "spark.executor.memory" -> "512M", "spark.driver.memory" -> "1024M", diff --git a/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/spark/SparkOnKubernetesTestsSuite.scala b/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/spark/SparkOnKubernetesTestsSuite.scala index 09532efe3d1..ea804575ecb 100644 --- a/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/spark/SparkOnKubernetesTestsSuite.scala +++ b/integration-tests/kyuubi-kubernetes-it/src/test/scala/org/apache/kyuubi/kubernetes/test/spark/SparkOnKubernetesTestsSuite.scala @@ -50,7 +50,7 @@ abstract class SparkOnKubernetesSuiteBase // TODO Support more Spark version // Spark official docker image: https://hub.docker.com/r/apache/spark/tags KyuubiConf().set("spark.master", s"k8s://$apiServerAddress") - .set("spark.kubernetes.container.image", "apache/spark:3.4.1") + .set("spark.kubernetes.container.image", "apache/spark:3.4.2") .set("spark.kubernetes.container.image.pullPolicy", "IfNotPresent") .set("spark.executor.instances", "1") .set("spark.executor.memory", "512M") diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 35d0b4f9ea7..d28f391b4c2 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -31,6 +31,7 @@ kyuubi-flink-it + kyuubi-gluten-it kyuubi-hive-it kyuubi-trino-it kyuubi-jdbc-it diff --git a/kyuubi-assembly/pom.xml b/kyuubi-assembly/pom.xml index 4fa0d9a0fd3..a853ac7f5e6 100644 --- a/kyuubi-assembly/pom.xml +++ b/kyuubi-assembly/pom.xml @@ -70,7 +70,7 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} + ${kyuubi-relocated-zookeeper.artifacts} diff --git a/kyuubi-common/pom.xml b/kyuubi-common/pom.xml index 0d5c491b51c..c9d32b148ff 100644 --- a/kyuubi-common/pom.xml +++ b/kyuubi-common/pom.xml @@ -99,18 +99,8 @@ - org.apache.thrift - libfb303 - - - - org.apache.thrift - libthrift - - - - org.apache.hive - hive-service-rpc + org.apache.kyuubi + kyuubi-relocated-hive-service-rpc diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala index 570ee6d3873..42579fb962f 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala @@ -23,9 +23,8 @@ import java.sql.SQLException import scala.annotation.tailrec import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TStatus, TStatusCode} - import org.apache.kyuubi.Utils.stringifyException +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TStatus, TStatusCode} import org.apache.kyuubi.util.reflect.DynConstructors /** diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala index accfca4c98f..896ed9df29d 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/Utils.scala @@ -40,6 +40,7 @@ import org.apache.hadoop.util.ShutdownHookManager import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.internal.Tests.IS_TESTING +import org.apache.kyuubi.util.command.CommandLineUtils._ object Utils extends Logging { @@ -325,7 +326,7 @@ object Utils extends Logging { require(args.length % 2 == 0, s"Illegal size of arguments.") for (i <- args.indices by 2) { require( - args(i) == "--conf", + args(i) == CONF, s"Unrecognized main arguments prefix ${args(i)}," + s"the argument format is '--conf k=v'.") @@ -336,25 +337,24 @@ object Utils extends Logging { } } - val REDACTION_REPLACEMENT_TEXT = "*********(redacted)" - - private val PATTERN_FOR_KEY_VALUE_ARG = "(.+?)=(.+)".r - - def redactCommandLineArgs(conf: KyuubiConf, commands: Array[String]): Array[String] = { - val redactionPattern = conf.get(SERVER_SECRET_REDACTION_PATTERN) - var nextKV = false - commands.map { - case PATTERN_FOR_KEY_VALUE_ARG(key, value) if nextKV => - val (_, newValue) = redact(redactionPattern, Seq((key, value))).head - nextKV = false - s"$key=$newValue" - - case cmd if cmd == "--conf" => - nextKV = true - cmd - - case cmd => - cmd + def redactCommandLineArgs(conf: KyuubiConf, commands: Iterable[String]): Iterable[String] = { + conf.get(SERVER_SECRET_REDACTION_PATTERN) match { + case Some(redactionPattern) => + var nextKV = false + commands.map { + case PATTERN_FOR_KEY_VALUE_ARG(key, value) if nextKV => + val (_, newValue) = redact(redactionPattern, Seq((key, value))).head + nextKV = false + genKeyValuePair(key, newValue) + + case cmd if cmd == CONF => + nextKV = true + cmd + + case cmd => + cmd + } + case _ => commands } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/cli/Handle.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/cli/Handle.scala index dd1ccb8fd90..2e90b105ec1 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/cli/Handle.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/cli/Handle.scala @@ -20,7 +20,7 @@ package org.apache.kyuubi.cli import java.nio.ByteBuffer import java.util.UUID -import org.apache.hive.service.rpc.thrift.THandleIdentifier +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.THandleIdentifier private[kyuubi] object Handle { final private val SECRET_ID = UUID.fromString("c2ee5b97-3ea0-41fc-ac16-9bd708ed8f38") diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala index a5c0aee0a32..784b82cddeb 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala @@ -1231,6 +1231,54 @@ object KyuubiConf { .checkValue(_ > 0, "must be positive number") .createWithDefault(Duration.ofMinutes(5).toMillis) + val KUBERNETES_SPARK_CLEANUP_TERMINATED_DRIVER_POD_KIND_CHECK_INTERVAL: ConfigEntry[Long] = + buildConf("kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.checkInterval") + .doc("Kyuubi server use guava cache as the cleanup trigger with time-based eviction, " + + "but the eviction would not happened until any get/put operation happened. " + + "This option schedule a daemon thread evict cache periodically.") + .version("1.8.1") + .timeConf + .createWithDefaultString("PT1M") + + val KUBERNETES_SPARK_CLEANUP_TERMINATED_DRIVER_POD_KIND: ConfigEntry[String] = + buildConf("kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.kind") + .doc("Kyuubi server will delete the spark driver pod after " + + s"the application terminates for ${KUBERNETES_TERMINATED_APPLICATION_RETAIN_PERIOD.key}. " + + "Available options are NONE, ALL, COMPLETED and " + + "default value is None which means none of the pod will be deleted") + .version("1.8.1") + .stringConf + .createWithDefault(KubernetesCleanupDriverPodStrategy.NONE.toString) + + object KubernetesCleanupDriverPodStrategy extends Enumeration { + type KubernetesCleanupDriverPodStrategy = Value + val NONE, ALL, COMPLETED = Value + } + + val KUBERNETES_APPLICATION_STATE_CONTAINER: ConfigEntry[String] = + buildConf("kyuubi.kubernetes.application.state.container") + .doc("The container name to retrieve the application state from.") + .version("1.8.1") + .stringConf + .createWithDefault("spark-kubernetes-driver") + + val KUBERNETES_APPLICATION_STATE_SOURCE: ConfigEntry[String] = + buildConf("kyuubi.kubernetes.application.state.source") + .doc("The source to retrieve the application state from. The valid values are " + + "pod and container. If the source is container and there is container inside the pod " + + s"with the name of ${KUBERNETES_APPLICATION_STATE_CONTAINER.key}, the application state " + + s"will be from the matched container state. " + + s"Otherwise, the application state will be from the pod state.") + .version("1.8.1") + .stringConf + .checkValues(KubernetesApplicationStateSource) + .createWithDefault(KubernetesApplicationStateSource.POD.toString) + + object KubernetesApplicationStateSource extends Enumeration { + type KubernetesApplicationStateSource = Value + val POD, CONTAINER = Value + } + // /////////////////////////////////////////////////////////////////////////////////////////////// // SQL Engine Configuration // // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -1426,7 +1474,7 @@ object KyuubiConf { val ENGINE_ALIVE_MAX_FAILURES: ConfigEntry[Int] = buildConf("kyuubi.session.engine.alive.max.failures") .doc("The maximum number of failures allowed for the engine.") - .version("1.8.0") + .version("1.8.1") .intConf .checkValue(_ > 0, "Must be positive") .createWithDefault(3) @@ -1468,6 +1516,22 @@ object KyuubiConf { .timeConf .createWithDefault(Duration.ofSeconds(10).toMillis) + object EngineOpenOnFailure extends Enumeration { + type EngineOpenOnFailure = Value + val RETRY, DEREGISTER_IMMEDIATELY, DEREGISTER_AFTER_RETRY = Value + } + + val ENGINE_OPEN_ON_FAILURE: ConfigEntry[String] = + buildConf("kyuubi.session.engine.open.onFailure") + .doc("The behavior when opening engine failed:
      " + + s"
    • RETRY: retry to open engine for ${ENGINE_OPEN_MAX_ATTEMPTS.key} times.
    • " + + "
    • DEREGISTER_IMMEDIATELY: deregister the engine immediately.
    • " + + "
    • DEREGISTER_AFTER_RETRY: deregister the engine after retry to open engine for " + + s"${ENGINE_OPEN_MAX_ATTEMPTS.key} times.
    ") + .version("1.8.1") + .stringConf + .createWithDefault(EngineOpenOnFailure.RETRY.toString) + val ENGINE_INIT_TIMEOUT: ConfigEntry[Long] = buildConf("kyuubi.session.engine.initialize.timeout") .doc("Timeout for starting the background engine, e.g. SparkSQLEngine.") .version("1.0.0") @@ -1893,6 +1957,33 @@ object KyuubiConf { .intConf .createWithDefault(0) + val OPERATION_RESULT_SAVE_TO_FILE: ConfigEntry[Boolean] = + buildConf("kyuubi.operation.result.saveToFile.enabled") + .doc("The switch for Spark query result save to file.") + .version("1.9.0") + .booleanConf + .createWithDefault(false) + + val OPERATION_RESULT_SAVE_TO_FILE_DIR: ConfigEntry[String] = + buildConf("kyuubi.operation.result.saveToFile.dir") + .doc("The Spark query result save dir, it should be a public accessible to every engine." + + " Results are saved in ORC format, and the directory structure is" + + " `/OPERATION_RESULT_SAVE_TO_FILE_DIR/engineId/sessionId/statementId`." + + " Each query result will delete when query finished.") + .version("1.9.0") + .stringConf + .createWithDefault("/tmp/kyuubi/tmp_kyuubi_result") + + val OPERATION_RESULT_SAVE_TO_FILE_MINSIZE: ConfigEntry[Long] = + buildConf("kyuubi.operation.result.saveToFile.minSize") + .doc("The minSize of Spark result save to file, default value is 200 MB." + + "we use spark's `EstimationUtils#getSizePerRowestimate` to estimate" + + " the output size of the execution plan.") + .version("1.9.0") + .longConf + .checkValue(_ > 0, "must be positive value") + .createWithDefault(200 * 1024 * 1024) + val OPERATION_INCREMENTAL_COLLECT: ConfigEntry[Boolean] = buildConf("kyuubi.operation.incremental.collect") .internal @@ -1932,6 +2023,16 @@ object KyuubiConf { .stringConf .createWithDefault("server_operation_logs") + val PROXY_USER: OptionalConfigEntry[String] = + buildConf("kyuubi.session.proxy.user") + .doc("An alternative to hive.server2.proxy.user. " + + "The current behavior is consistent with hive.server2.proxy.user " + + "and now only takes effect in RESTFul API. " + + "When both parameters are set, kyuubi.session.proxy.user takes precedence.") + .version("1.9.0") + .stringConf + .createOptional + @deprecated("using kyuubi.engine.share.level instead", "1.2.0") val LEGACY_ENGINE_SHARE_LEVEL: ConfigEntry[String] = buildConf("kyuubi.session.engine.share.level") @@ -2015,7 +2116,7 @@ object KyuubiConf { " all the capacity of the Hive Server2." + "
  • JDBC: specify this engine type will launch a JDBC engine which can forward " + " queries to the database system through the certain JDBC driver, " + - " for now, it supports Doris and Phoenix.
  • " + + " for now, it supports Doris, MySQL, Phoenix, PostgreSQL and StarRocks." + "
  • CHAT: specify this engine type will launch a Chat engine.
  • " + "") .version("1.4.0") @@ -2091,6 +2192,13 @@ object KyuubiConf { .toSequence(";") .createWithDefault(Nil) + val ENGINE_SESSION_FLINK_INITIALIZE_SQL: ConfigEntry[Seq[String]] = + buildConf("kyuubi.session.engine.flink.initialize.sql") + .doc("The initialize sql for Flink session. " + + "It fallback to `kyuubi.engine.session.initialize.sql`") + .version("1.8.1") + .fallbackConf(ENGINE_SESSION_INITIALIZE_SQL) + val ENGINE_DEREGISTER_EXCEPTION_CLASSES: ConfigEntry[Set[String]] = buildConf("kyuubi.engine.deregister.exception.classes") .doc("A comma-separated list of exception classes. If there is any exception thrown," + @@ -2468,14 +2576,15 @@ object KyuubiConf { .checkValues(OperationLanguages) .createWithDefault(OperationLanguages.SQL.toString) - val SESSION_CONF_ADVISOR: OptionalConfigEntry[String] = + val SESSION_CONF_ADVISOR: OptionalConfigEntry[Seq[String]] = buildConf("kyuubi.session.conf.advisor") - .doc("A config advisor plugin for Kyuubi Server. This plugin can provide some custom " + + .doc("A config advisor plugin for Kyuubi Server. This plugin can provide a list of custom " + "configs for different users or session configs and overwrite the session configs before " + "opening a new session. This config value should be a subclass of " + "`org.apache.kyuubi.plugin.SessionConfAdvisor` which has a zero-arg constructor.") .version("1.5.0") .stringConf + .toSequence() .createOptional val GROUP_PROVIDER: ConfigEntry[String] = @@ -2533,6 +2642,13 @@ object KyuubiConf { .stringConf .createWithDefault("yyyy-MM-dd HH:mm:ss.SSS") + val ENGINE_SESSION_SPARK_INITIALIZE_SQL: ConfigEntry[Seq[String]] = + buildConf("kyuubi.session.engine.spark.initialize.sql") + .doc("The initialize sql for Spark session. " + + "It fallback to `kyuubi.engine.session.initialize.sql`") + .version("1.8.1") + .fallbackConf(ENGINE_SESSION_INITIALIZE_SQL) + val ENGINE_TRINO_MEMORY: ConfigEntry[String] = buildConf("kyuubi.engine.trino.memory") .doc("The heap memory for the Trino query engine") @@ -2607,6 +2723,12 @@ object KyuubiConf { .stringConf .createOptional + val ENGINE_FLINK_INITIALIZE_SQL: ConfigEntry[Seq[String]] = + buildConf("kyuubi.engine.flink.initialize.sql") + .doc("The initialize sql for Flink engine. It fallback to `kyuubi.engine.initialize.sql`.") + .version("1.8.1") + .fallbackConf(ENGINE_INITIALIZE_SQL) + val SERVER_LIMIT_CONNECTIONS_PER_USER: OptionalConfigEntry[Int] = buildConf("kyuubi.server.limit.connections.per.user") .doc("Maximum kyuubi server connections per user." + @@ -2717,7 +2839,9 @@ object KyuubiConf { val SERVER_ADMINISTRATORS: ConfigEntry[Set[String]] = buildConf("kyuubi.server.administrators") .doc("Comma-separated list of Kyuubi service administrators. " + - "We use this config to grant admin permission to any service accounts.") + "We use this config to grant admin permission to any service accounts when " + + s"security mechanism is enabled. Note, when ${AUTHENTICATION_METHOD.key} is " + + "configured to NOSASL or NONE, everyone is treated as administrator.") .version("1.8.0") .serverOnly .stringConf @@ -2777,9 +2901,31 @@ object KyuubiConf { val ENGINE_JDBC_CONNECTION_PROVIDER: OptionalConfigEntry[String] = buildConf("kyuubi.engine.jdbc.connection.provider") - .doc("The connection provider is used for getting a connection from the server") + .doc("A JDBC connection provider plugin for the Kyuubi Server " + + "to establish a connection to the JDBC URL." + + " The configuration value should be a subclass of " + + "`org.apache.kyuubi.engine.jdbc.connection.JdbcConnectionProvider`. " + + "Kyuubi provides the following built-in implementations: " + + "
  • doris: For establishing Doris connections.
  • " + + "
  • mysql: For establishing MySQL connections.
  • " + + "
  • phoenix: For establishing Phoenix connections.
  • " + + "
  • postgresql: For establishing PostgreSQL connections.
  • " + + "
  • starrocks: For establishing StarRocks connections.
  • ") .version("1.6.0") .stringConf + .transform { + case "Doris" | "doris" | "DorisConnectionProvider" => + "org.apache.kyuubi.engine.jdbc.doris.DorisConnectionProvider" + case "MySQL" | "mysql" | "MySQLConnectionProvider" => + "org.apache.kyuubi.engine.jdbc.mysql.MySQLConnectionProvider" + case "Phoenix" | "phoenix" | "PhoenixConnectionProvider" => + "org.apache.kyuubi.engine.jdbc.phoenix.PhoenixConnectionProvider" + case "PostgreSQL" | "postgresql" | "PostgreSQLConnectionProvider" => + "org.apache.kyuubi.engine.jdbc.postgresql.PostgreSQLConnectionProvider" + case "StarRocks" | "starrocks" | "StarRocksConnectionProvider" => + "org.apache.kyuubi.engine.jdbc.starrocks.StarRocksConnectionProvider" + case other => other + } .createOptional val ENGINE_JDBC_SHORT_NAME: OptionalConfigEntry[String] = @@ -2807,6 +2953,13 @@ object KyuubiConf { .toSequence(";") .createWithDefault(Nil) + val ENGINE_JDBC_FETCH_SIZE: ConfigEntry[Int] = + buildConf("kyuubi.engine.jdbc.fetch.size") + .doc("The fetch size of JDBC engine") + .version("1.9.0") + .intConf + .createWithDefault(1000) + val ENGINE_OPERATION_CONVERT_CATALOG_DATABASE_ENABLED: ConfigEntry[Boolean] = buildConf("kyuubi.engine.operation.convert.catalog.database.enabled") .doc("When set to true, The engine converts the JDBC methods of set/get Catalog " + @@ -2952,12 +3105,15 @@ object KyuubiConf { .doc("The provider for the Chat engine. Candidates:
      " + "
    • ECHO: simply replies a welcome message.
    • " + "
    • GPT: a.k.a ChatGPT, powered by OpenAI.
    • " + + "
    • ERNIE: ErnieBot, powered by Baidu.
    • " + "
    ") .version("1.8.0") .stringConf .transform { case "ECHO" | "echo" => "org.apache.kyuubi.engine.chat.provider.EchoProvider" case "GPT" | "gpt" | "ChatGPT" => "org.apache.kyuubi.engine.chat.provider.ChatGPTProvider" + case "ERNIE" | "ernie" | "ErnieBot" => + "org.apache.kyuubi.engine.chat.provider.ErnieBotProvider" case other => other } .createWithDefault("ECHO") @@ -2978,6 +3134,23 @@ object KyuubiConf { .stringConf .createWithDefault("gpt-3.5-turbo") + val ENGINE_ERNIE_BOT_ACCESS_TOKEN: OptionalConfigEntry[String] = + buildConf("kyuubi.engine.chat.ernie.token") + .doc("The token to access ernie bot open API, which could be got at " + + "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5") + .version("1.9.0") + .stringConf + .createOptional + + val ENGINE_ERNIE_BOT_MODEL: ConfigEntry[String] = + buildConf("kyuubi.engine.chat.ernie.model") + .doc("ID of the model used in ernie bot. " + + "Available models are completions_pro, ernie_bot_8k, completions and eb-instant" + + "[Model overview](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/6lp69is2a).") + .version("1.9.0") + .stringConf + .createWithDefault("completions") + val ENGINE_CHAT_EXTRA_CLASSPATH: OptionalConfigEntry[String] = buildConf("kyuubi.engine.chat.extra.classpath") .doc("The extra classpath for the Chat engine, for configuring the location " + @@ -2993,6 +3166,13 @@ object KyuubiConf { .stringConf .createOptional + val ENGINE_ERNIE_BOT_HTTP_PROXY: OptionalConfigEntry[String] = + buildConf("kyuubi.engine.chat.ernie.http.proxy") + .doc("HTTP proxy url for API calling in ernie bot engine. e.g. http://127.0.0.1:1088") + .version("1.9.0") + .stringConf + .createOptional + val ENGINE_CHAT_GPT_HTTP_CONNECT_TIMEOUT: ConfigEntry[Long] = buildConf("kyuubi.engine.chat.gpt.http.connect.timeout") .doc("The timeout[ms] for establishing the connection with the Chat GPT server. " + @@ -3002,6 +3182,15 @@ object KyuubiConf { .checkValue(_ >= 0, "must be 0 or positive number") .createWithDefault(Duration.ofSeconds(120).toMillis) + val ENGINE_ERNIE_HTTP_CONNECT_TIMEOUT: ConfigEntry[Long] = + buildConf("kyuubi.engine.chat.ernie.http.connect.timeout") + .doc("The timeout[ms] for establishing the connection with the ernie bot server. " + + "A timeout value of zero is interpreted as an infinite timeout.") + .version("1.9.0") + .timeConf + .checkValue(_ >= 0, "must be 0 or positive number") + .createWithDefault(Duration.ofSeconds(120).toMillis) + val ENGINE_CHAT_GPT_HTTP_SOCKET_TIMEOUT: ConfigEntry[Long] = buildConf("kyuubi.engine.chat.gpt.http.socket.timeout") .doc("The timeout[ms] for waiting for data packets after Chat GPT server " + @@ -3011,6 +3200,15 @@ object KyuubiConf { .checkValue(_ >= 0, "must be 0 or positive number") .createWithDefault(Duration.ofSeconds(120).toMillis) + val ENGINE_ERNIE_HTTP_SOCKET_TIMEOUT: ConfigEntry[Long] = + buildConf("kyuubi.engine.chat.ernie.http.socket.timeout") + .doc("The timeout[ms] for waiting for data packets after ernie bot server " + + "connection is established. A timeout value of zero is interpreted as an infinite timeout.") + .version("1.9.0") + .timeConf + .checkValue(_ >= 0, "must be 0 or positive number") + .createWithDefault(Duration.ofSeconds(120).toMillis) + val ENGINE_JDBC_MEMORY: ConfigEntry[String] = buildConf("kyuubi.engine.jdbc.memory") .doc("The heap memory for the JDBC query engine") @@ -3067,6 +3265,29 @@ object KyuubiConf { .stringConf .createWithDefault("bin/python") + val ENGINE_SPARK_PYTHON_MAGIC_ENABLED: ConfigEntry[Boolean] = + buildConf("kyuubi.engine.spark.python.magic.enabled") + .internal + .doc("Whether to enable pyspark magic node, which is helpful for notebook." + + " See details in KYUUBI #5877") + .version("1.9.0") + .booleanConf + .createWithDefault(true) + + object EngineSparkOutputMode extends Enumeration { + type EngineSparkOutputMode = Value + val AUTO, NOTEBOOK = Value + } + + val ENGINE_SPARK_OUTPUT_MODE: ConfigEntry[String] = + buildConf("kyuubi.engine.spark.output.mode") + .doc("The output mode of Spark engine:
      " + + "
    • AUTO: For PySpark, the extracted `text/plain` from python response as output.
    • " + + "
    • NOTEBOOK: For PySpark, the original python response as output.
    ") + .version("1.9.0") + .stringConf + .createWithDefault(EngineSparkOutputMode.AUTO.toString) + val ENGINE_SPARK_REGISTER_ATTRIBUTES: ConfigEntry[Seq[String]] = buildConf("kyuubi.engine.spark.register.attributes") .internal @@ -3076,6 +3297,12 @@ object KyuubiConf { .toSequence() .createWithDefault(Seq("spark.driver.memory", "spark.executor.memory")) + val ENGINE_SPARK_INITIALIZE_SQL: ConfigEntry[Seq[String]] = + buildConf("kyuubi.engine.spark.initialize.sql") + .doc("The initialize sql for Spark engine. It fallback to `kyuubi.engine.initialize.sql`.") + .version("1.8.1") + .fallbackConf(ENGINE_INITIALIZE_SQL) + val ENGINE_HIVE_EVENT_LOGGERS: ConfigEntry[Seq[String]] = buildConf("kyuubi.engine.hive.event.loggers") .doc("A comma-separated list of engine history loggers, where engine/session/operation etc" + @@ -3150,4 +3377,24 @@ object KyuubiConf { .serverOnly .intConf .createOptional + + val KUBERNETES_FORCIBLY_REWRITE_DRIVER_POD_NAME: ConfigEntry[Boolean] = + buildConf("kyuubi.kubernetes.spark.forciblyRewriteDriverPodName.enabled") + .doc("Whether to forcibly rewrite Spark driver pod name with 'kyuubi--driver'. " + + "If disabled, Kyuubi will try to preserve the application name while satisfying K8s' " + + "pod name policy, but some vendors may have stricter pod name policies, thus the " + + "generated name may become illegal.") + .version("1.8.1") + .booleanConf + .createWithDefault(false) + + val KUBERNETES_FORCIBLY_REWRITE_EXEC_POD_NAME_PREFIX: ConfigEntry[Boolean] = + buildConf("kyuubi.kubernetes.spark.forciblyRewriteExecutorPodNamePrefix.enabled") + .doc("Whether to forcibly rewrite Spark executor pod name prefix with 'kyuubi-'. " + + "If disabled, Kyuubi will try to preserve the application name while satisfying K8s' " + + "pod name policy, but some vendors may have stricter Pod name policies, thus the " + + "generated name may become illegal.") + .version("1.8.1") + .booleanConf + .createWithDefault(false) } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnGenerator.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnGenerator.scala new file mode 100644 index 00000000000..e2c8f1ea6e5 --- /dev/null +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnGenerator.scala @@ -0,0 +1,105 @@ +/* + * 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.kyuubi.engine.result +import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float => JFloat, Long => JLong, Short => JShort} +import java.nio.ByteBuffer +import java.util.{ArrayList => JArrayList, BitSet => JBitSet, List => JList} + +import scala.collection.JavaConverters._ + +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +trait TColumnGenerator[RowT] extends TRowSetColumnGetter[RowT] { + protected def getColumnToList[T]( + rows: Seq[RowT], + ordinal: Int, + defaultVal: T, + convertFunc: (RowT, Int) => T = null): (JList[T], ByteBuffer) = { + val rowSize = rows.length + val ret = new JArrayList[T](rowSize) + val nulls = new JBitSet() + var idx = 0 + while (idx < rowSize) { + val row = rows(idx) + val isNull = isColumnNullAt(row, ordinal) + if (isNull) { + nulls.set(idx, true) + ret.add(defaultVal) + } else { + val value = Option(convertFunc) match { + case Some(f) => f(row, ordinal) + case _ => getColumnAs[T](row, ordinal) + } + ret.add(value) + } + idx += 1 + } + (ret, ByteBuffer.wrap(nulls.toByteArray)) + } + + def asBooleanTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JBoolean](rows, ordinal, true) + TColumn.boolVal(new TBoolColumn(values, nulls)) + } + + def asByteTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JByte](rows, ordinal, 0.toByte) + TColumn.byteVal(new TByteColumn(values, nulls)) + } + + def asShortTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JShort](rows, ordinal, 0.toShort) + TColumn.i16Val(new TI16Column(values, nulls)) + } + + def asIntegerTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[Integer](rows, ordinal, 0) + TColumn.i32Val(new TI32Column(values, nulls)) + } + + def asLongTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JLong](rows, ordinal, 0.toLong) + TColumn.i64Val(new TI64Column(values, nulls)) + } + + def asFloatTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JFloat](rows, ordinal, 0.toFloat) + val doubleValues = values.asScala.map(f => JDouble.valueOf(f.toString)).asJava + TColumn.doubleVal(new TDoubleColumn(doubleValues, nulls)) + } + + def asDoubleTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[JDouble](rows, ordinal, 0.toDouble) + TColumn.doubleVal(new TDoubleColumn(values, nulls)) + } + + def asStringTColumn( + rows: Seq[RowT], + ordinal: Int, + defaultVal: String = "", + convertFunc: (RowT, Int) => String = null): TColumn = { + val (values, nulls) = getColumnToList[String](rows, ordinal, defaultVal, convertFunc) + TColumn.stringVal(new TStringColumn(values, nulls)) + } + + def asByteArrayTColumn(rows: Seq[RowT], ordinal: Int): TColumn = { + val (values, nulls) = getColumnToList[Array[Byte]](rows, ordinal, defaultVal = Array[Byte]()) + val byteBufferValues = values.asScala.map(ByteBuffer.wrap).asJava + TColumn.binaryVal(new TBinaryColumn(byteBufferValues, nulls)) + } +} diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnValueGenerator.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnValueGenerator.scala new file mode 100644 index 00000000000..0ff3a250df7 --- /dev/null +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TColumnValueGenerator.scala @@ -0,0 +1,97 @@ +/* + * 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.kyuubi.engine.result + +import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float => JFloat, Long => JLong, Short => JShort} + +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +trait TColumnValueGenerator[RowT] extends TRowSetColumnGetter[RowT] { + + def asBooleanTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TBoolValue + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JBoolean](row, ordinal)) + } + TColumnValue.boolVal(tValue) + } + + def asByteTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TByteValue + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JByte](row, ordinal)) + } + TColumnValue.byteVal(tValue) + } + + def asShortTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TI16Value + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JShort](row, ordinal)) + } + TColumnValue.i16Val(tValue) + } + + def asIntegerTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TI32Value + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[Integer](row, ordinal)) + } + TColumnValue.i32Val(tValue) + } + + def asLongTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TI64Value + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JLong](row, ordinal)) + } + TColumnValue.i64Val(tValue) + } + + def asFloatTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TDoubleValue + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JFloat](row, ordinal).toDouble) + } + TColumnValue.doubleVal(tValue) + } + + def asDoubleTColumnValue(row: RowT, ordinal: Int): TColumnValue = { + val tValue = new TDoubleValue + if (!isColumnNullAt(row, ordinal)) { + tValue.setValue(getColumnAs[JDouble](row, ordinal)) + } + TColumnValue.doubleVal(tValue) + } + + def asStringTColumnValue( + row: RowT, + ordinal: Int, + convertFunc: Any => String = null): TColumnValue = { + val tValue = new TStringValue + if (!isColumnNullAt(row, ordinal)) { + val str = getColumnAs[Any](row, ordinal) match { + case strObj: String => strObj + case obj if convertFunc != null => convertFunc(obj) + case anyObj => String.valueOf(anyObj) + } + tValue.setValue(str) + } + TColumnValue.stringVal(tValue) + } +} diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetColumnGetter.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetColumnGetter.scala new file mode 100644 index 00000000000..3f6b6a16ada --- /dev/null +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetColumnGetter.scala @@ -0,0 +1,24 @@ +/* + * 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.kyuubi.engine.result + +trait TRowSetColumnGetter[RowT] { + protected def isColumnNullAt(row: RowT, ordinal: Int): Boolean + + protected def getColumnAs[T](row: RowT, ordinal: Int): T +} diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetGenerator.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetGenerator.scala new file mode 100644 index 00000000000..096e45ad8a9 --- /dev/null +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/result/TRowSetGenerator.scala @@ -0,0 +1,77 @@ +/* + * 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.kyuubi.engine.result +import java.util.{ArrayList => JArrayList} + +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ + +trait TRowSetGenerator[SchemaT, RowT, ColumnT] + extends TColumnValueGenerator[RowT] with TColumnGenerator[RowT] { + + def getColumnSizeFromSchemaType(schema: SchemaT): Int + + def getColumnType(schema: SchemaT, ordinal: Int): ColumnT + + def toTColumn(rows: Seq[RowT], ordinal: Int, typ: ColumnT): TColumn + + def toTColumnValue(row: RowT, ordinal: Int, types: SchemaT): TColumnValue + + def toTRowSet(rows: Seq[RowT], schema: SchemaT, protocolVersion: TProtocolVersion): TRowSet = { + if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { + toRowBasedSet(rows, schema) + } else { + toColumnBasedSet(rows, schema) + } + } + + def toRowBasedSet(rows: Seq[RowT], schema: SchemaT): TRowSet = { + val rowSize = rows.length + val tRows = new JArrayList[TRow](rowSize) + var i = 0 + while (i < rowSize) { + val row = rows(i) + var j = 0 + val columnSize = getColumnSizeFromSchemaType(schema) + val tColumnValues = new JArrayList[TColumnValue](columnSize) + while (j < columnSize) { + val columnValue = toTColumnValue(row, j, schema) + tColumnValues.add(columnValue) + j += 1 + } + i += 1 + val tRow = new TRow(tColumnValues) + tRows.add(tRow) + } + new TRowSet(0, tRows) + } + + def toColumnBasedSet(rows: Seq[RowT], schema: SchemaT): TRowSet = { + val rowSize = rows.length + val tRowSet = new TRowSet(0, new JArrayList[TRow](rowSize)) + var i = 0 + val columnSize = getColumnSizeFromSchemaType(schema) + val tColumns = new JArrayList[TColumn](columnSize) + while (i < columnSize) { + val tColumn = toTColumn(rows, i, getColumnType(schema, i)) + tColumns.add(tColumn) + i += 1 + } + tRowSet.setColumns(tColumns) + tRowSet + } +} diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/AbstractOperation.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/AbstractOperation.scala index 0a185b94266..05dd7fda907 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/AbstractOperation.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/AbstractOperation.scala @@ -17,13 +17,13 @@ package org.apache.kyuubi.operation +import java.io.IOException import java.util.concurrent.{Future, ScheduledExecutorService, TimeUnit} import java.util.concurrent.locks.ReentrantLock import scala.collection.JavaConverters._ import org.apache.commons.lang3.StringUtils -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TProgressUpdateResp, TProtocolVersion, TStatus, TStatusCode} import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils} import org.apache.kyuubi.config.KyuubiConf.OPERATION_IDLE_TIMEOUT @@ -31,6 +31,7 @@ import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.OperationState._ import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp, TProgressUpdateResp, TProtocolVersion, TStatus, TStatusCode} import org.apache.kyuubi.util.ThreadUtils abstract class AbstractOperation(session: Session) extends Operation with Logging { @@ -104,6 +105,7 @@ abstract class AbstractOperation(session: Session) extends Operation with Loggin this.operationException = opEx } + def getOperationJobProgress: TProgressUpdateResp = operationJobProgress def setOperationJobProgress(opJobProgress: TProgressUpdateResp): Unit = { this.operationJobProgress = opJobProgress } @@ -247,4 +249,19 @@ abstract class AbstractOperation(session: Session) extends Operation with Loggin ok.setInfoMessages(hints.asJava) ok } + + /** + * Close the OperationLog, after running the block + */ + def withClosingOperationLog[T](f: => T): T = { + try { + f + } finally { + try { + getOperationLog.foreach(_.close()) + } catch { + case e: IOException => error(e.getMessage, e) + } + } + } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/FetchOrientation.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/FetchOrientation.scala index b5136e91d20..71e9397072d 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/FetchOrientation.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/FetchOrientation.scala @@ -17,7 +17,7 @@ package org.apache.kyuubi.operation -import org.apache.hive.service.rpc.thrift.TFetchOrientation +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TFetchOrientation object FetchOrientation extends Enumeration { type FetchOrientation = Value diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/Operation.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/Operation.scala index c20a16f61d0..e216385180f 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/Operation.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/Operation.scala @@ -19,11 +19,10 @@ package org.apache.kyuubi.operation import java.util.concurrent.Future -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} - import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} trait Operation { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationHandle.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationHandle.scala index 419bdc9c471..9a93c549077 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationHandle.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationHandle.scala @@ -19,9 +19,8 @@ package org.apache.kyuubi.operation import java.util.UUID -import org.apache.hive.service.rpc.thrift.TOperationHandle - import org.apache.kyuubi.cli.Handle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationHandle case class OperationHandle(identifier: UUID) { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationManager.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationManager.scala index 38dabcc1a89..0b19e68ffb9 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationManager.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationManager.scala @@ -19,8 +19,6 @@ package org.apache.kyuubi.operation import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiReservedKeys._ @@ -29,6 +27,7 @@ import org.apache.kyuubi.operation.OperationState._ import org.apache.kyuubi.operation.log.LogDivertAppender import org.apache.kyuubi.service.AbstractService import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ /** * The [[OperationManager]] manages all the operations during their lifecycle. diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationState.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationState.scala index 67a517a23ec..7d00a8cf5f3 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationState.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationState.scala @@ -19,10 +19,9 @@ package org.apache.kyuubi.operation import scala.language.implicitConversions -import org.apache.hive.service.rpc.thrift.TOperationState -import org.apache.hive.service.rpc.thrift.TOperationState._ - import org.apache.kyuubi.KyuubiSQLException +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationState +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationState._ object OperationState extends Enumeration { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationStatus.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationStatus.scala index 9b139c9dc43..4ea82c32088 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationStatus.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/OperationStatus.scala @@ -17,10 +17,9 @@ package org.apache.kyuubi.operation -import org.apache.hive.service.rpc.thrift.TProgressUpdateResp - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.operation.OperationState.OperationState +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProgressUpdateResp case class OperationStatus( state: OperationState, diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/log/OperationLog.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/log/OperationLog.scala index 2e133df28b8..b3bd46d35a4 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/log/OperationLog.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/operation/log/OperationLog.scala @@ -26,12 +26,11 @@ import java.util.{ArrayList => JArrayList, List => JList} import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer -import org.apache.hive.service.rpc.thrift.{TColumn, TRow, TRowSet, TStringColumn} - import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FetchOrientation} import org.apache.kyuubi.operation.OperationHandle import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TRow, TRowSet, TStringColumn} import org.apache.kyuubi.util.ThriftUtils object OperationLog extends Logging { @@ -233,8 +232,6 @@ class OperationLog(path: Path) { } def close(): Unit = synchronized { - if (!initialized) return - closeExtraReaders() trySafely { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/AbstractBackendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/AbstractBackendService.scala index 443b353546e..0ecb6e38ffd 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/AbstractBackendService.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/AbstractBackendService.scala @@ -21,12 +21,11 @@ import java.util.concurrent.{ExecutionException, TimeoutException, TimeUnit} import scala.concurrent.CancellationException -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.operation.{OperationHandle, OperationStatus} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ /** * A Shorthand for implementing [[BackendService]]s diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/BackendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/BackendService.scala index 85df9024cc4..0f2691a01e0 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/BackendService.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/BackendService.scala @@ -17,11 +17,10 @@ package org.apache.kyuubi.service -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.operation.{OperationHandle, OperationStatus} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.{SessionHandle, SessionManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ /** * A [[BackendService]] in Kyuubi architecture is responsible for talking to the SQL engine diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala index 2f441937476..19e2e31eafe 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala @@ -23,13 +23,12 @@ import java.util.Locale import java.util.concurrent.{SynchronousQueue, ThreadPoolExecutor, TimeUnit} import javax.net.ssl.{KeyManagerFactory, SSLServerSocket} -import org.apache.hive.service.rpc.thrift._ -import org.apache.thrift.protocol.TBinaryProtocol -import org.apache.thrift.server.{TServer, TThreadPoolServer} -import org.apache.thrift.transport.{TServerSocket, TSSLTransportFactory} - import org.apache.kyuubi.{KyuubiException, Logging} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol +import org.apache.kyuubi.shaded.thrift.server.{TServer, TThreadPoolServer} +import org.apache.kyuubi.shaded.thrift.transport.{TServerSocket, TSSLTransportFactory} import org.apache.kyuubi.util.NamedThreadFactory /** diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala index 7cc23779fee..a742993c5ad 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala @@ -24,18 +24,18 @@ import scala.collection.JavaConverters._ import scala.language.implicitConversions import org.apache.hadoop.conf.Configuration -import org.apache.hive.service.rpc.thrift._ -import org.apache.thrift.protocol.TProtocol -import org.apache.thrift.server.{ServerContext, TServerEventHandler} -import org.apache.thrift.transport.TTransport import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils} import org.apache.kyuubi.Utils.stringifyException -import org.apache.kyuubi.config.KyuubiConf.{FRONTEND_ADVERTISED_HOST, FRONTEND_CONNECTION_URL_USE_HOSTNAME, SESSION_CLOSE_ON_DISCONNECT} +import org.apache.kyuubi.config.KyuubiConf.{FRONTEND_ADVERTISED_HOST, FRONTEND_CONNECTION_URL_USE_HOSTNAME, PROXY_USER, SESSION_CLOSE_ON_DISCONNECT} import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle} import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.protocol.TProtocol +import org.apache.kyuubi.shaded.thrift.server.{ServerContext, TServerEventHandler} +import org.apache.kyuubi.shaded.thrift.transport.TTransport import org.apache.kyuubi.util.{KyuubiHadoopUtils, NamedThreadFactory} /** @@ -127,7 +127,8 @@ abstract class TFrontendService(name: String) sessionConf: java.util.Map[String, String], ipAddress: String, realUser: String): String = { - val proxyUser = sessionConf.get(KyuubiAuthenticationFactory.HS2_PROXY_USER) + val proxyUser = Option(sessionConf.get(PROXY_USER.key)) + .getOrElse(sessionConf.get(KyuubiAuthenticationFactory.HS2_PROXY_USER)) if (proxyUser == null) { realUser } else { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/FEServiceProcessorFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/FEServiceProcessorFactory.scala index 79180314521..ea6156c362c 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/FEServiceProcessorFactory.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/FEServiceProcessorFactory.scala @@ -17,9 +17,9 @@ package org.apache.kyuubi.service.authentication -import org.apache.hive.service.rpc.thrift.TCLIService.{Iface, Processor} -import org.apache.thrift.{TProcessor, TProcessorFactory} -import org.apache.thrift.transport.TTransport +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.{Iface, Processor} +import org.apache.kyuubi.shaded.thrift.{TProcessor, TProcessorFactory} +import org.apache.kyuubi.shaded.thrift.transport.TTransport private[authentication] case class FEServiceProcessorFactory( saslServer: HadoopThriftAuthBridgeServer, diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/HadoopThriftAuthBridgeServer.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/HadoopThriftAuthBridgeServer.scala index 5f5c7000823..6c1dfa5daee 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/HadoopThriftAuthBridgeServer.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/HadoopThriftAuthBridgeServer.scala @@ -28,11 +28,11 @@ import org.apache.hadoop.fs.FileSystem import org.apache.hadoop.security.{SaslRpcServer, UserGroupInformation} import org.apache.hadoop.security.SaslRpcServer.AuthMethod import org.apache.hadoop.security.token.SecretManager.InvalidToken -import org.apache.thrift.{TException, TProcessor} -import org.apache.thrift.protocol.TProtocol -import org.apache.thrift.transport._ import org.apache.kyuubi.Logging +import org.apache.kyuubi.shaded.thrift.{TException, TProcessor} +import org.apache.kyuubi.shaded.thrift.protocol.TProtocol +import org.apache.kyuubi.shaded.thrift.transport._ class HadoopThriftAuthBridgeServer(secretMgr: KyuubiDelegationTokenManager) { import HadoopThriftAuthBridgeServer._ diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala index 1b62f6030e7..736f8e1e15e 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala @@ -25,22 +25,21 @@ import org.apache.hadoop.conf.Configuration import org.apache.hadoop.security.UserGroupInformation import org.apache.hadoop.security.authentication.util.KerberosName import org.apache.hadoop.security.authorize.ProxyUsers -import org.apache.hive.service.rpc.thrift.TCLIService.Iface -import org.apache.thrift.TProcessorFactory -import org.apache.thrift.transport.{TSaslServerTransport, TTransportException, TTransportFactory} import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.service.authentication.AuthMethods.AuthMethod import org.apache.kyuubi.service.authentication.AuthTypes._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface +import org.apache.kyuubi.shaded.thrift.TProcessorFactory +import org.apache.kyuubi.shaded.thrift.transport.{TSaslServerTransport, TTransportException, TTransportFactory} class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) extends Logging { - private val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) - private val none = authTypes.contains(NONE) - private val noSasl = authTypes == Set(NOSASL) - private val kerberosEnabled = authTypes.contains(KERBEROS) + val authTypes: Set[AuthType] = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + val noSaslEnabled: Boolean = authTypes == Set(NOSASL) + val kerberosEnabled: Boolean = authTypes.contains(KERBEROS) private val plainAuthTypeOpt = authTypes.filterNot(_.equals(KERBEROS)) .filterNot(_.equals(NOSASL)).headOption @@ -71,7 +70,7 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex } def getTTransportFactory: TTransportFactory = { - if (noSasl) { + if (noSaslEnabled) { new TTransportFactory() } else { var transportFactory: TSaslServerTransport.Factory = null @@ -119,33 +118,8 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex hadoopAuthServer.map(_.getRemoteAddress).map(_.getHostAddress) .orElse(Option(TSetIpAddressProcessor.getUserIpAddress)) } - - def isNoSaslEnabled: Boolean = { - noSasl - } - - def isKerberosEnabled: Boolean = { - kerberosEnabled - } - - def isPlainAuthEnabled: Boolean = { - plainAuthTypeOpt.isDefined - } - - def isNoneEnabled: Boolean = { - none - } - - def getValidPasswordAuthMethod: AuthMethod = { - debug(authTypes) - if (none) AuthMethods.NONE - else if (authTypes.contains(LDAP)) AuthMethods.LDAP - else if (authTypes.contains(JDBC)) AuthMethods.JDBC - else if (authTypes.contains(CUSTOM)) AuthMethods.CUSTOM - else throw new IllegalArgumentException("No valid Password Auth detected") - } } -object KyuubiAuthenticationFactory { +object KyuubiAuthenticationFactory extends Logging { val HS2_PROXY_USER = "hive.server2.proxy.user" @throws[KyuubiSQLException] @@ -177,4 +151,13 @@ object KyuubiAuthenticationFactory { e) } } + + def getValidPasswordAuthMethod(authTypes: Set[AuthType]): AuthMethod = { + if (authTypes == Set(NOSASL)) AuthMethods.NONE + else if (authTypes.contains(NONE)) AuthMethods.NONE + else if (authTypes.contains(LDAP)) AuthMethods.LDAP + else if (authTypes.contains(JDBC)) AuthMethods.JDBC + else if (authTypes.contains(CUSTOM)) AuthMethods.CUSTOM + else throw new IllegalArgumentException("No valid Password Auth detected") + } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala index 3959341ed5f..2d880a344e6 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala @@ -22,13 +22,12 @@ import java.util.Collections import javax.security.auth.callback.{Callback, CallbackHandler, NameCallback, PasswordCallback, UnsupportedCallbackException} import javax.security.sasl.AuthorizeCallback -import org.apache.hive.service.rpc.thrift.TCLIService.Iface -import org.apache.thrift.{TProcessor, TProcessorFactory} -import org.apache.thrift.transport.{TSaslClientTransport, TSaslServerTransport, TTransport, TTransportFactory} - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.service.authentication.AuthMethods.AuthMethod import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface +import org.apache.kyuubi.shaded.thrift.{TProcessor, TProcessorFactory} +import org.apache.kyuubi.shaded.thrift.transport.{TSaslClientTransport, TSaslServerTransport, TTransport, TTransportFactory} object PlainSASLHelper { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/TSetIpAddressProcessor.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/TSetIpAddressProcessor.scala index ebf82f26f44..6a890593642 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/TSetIpAddressProcessor.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/TSetIpAddressProcessor.scala @@ -17,12 +17,11 @@ package org.apache.kyuubi.service.authentication -import org.apache.hive.service.rpc.thrift.TCLIService.{Iface, Processor} -import org.apache.thrift.TException -import org.apache.thrift.protocol.TProtocol -import org.apache.thrift.transport.{TSaslClientTransport, TSaslServerTransport, TSocket, TTransport} - import org.apache.kyuubi.Logging +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.{Iface, Processor} +import org.apache.kyuubi.shaded.thrift.TException +import org.apache.kyuubi.shaded.thrift.protocol.TProtocol +import org.apache.kyuubi.shaded.thrift.transport.{TSaslClientTransport, TSaslServerTransport, TSocket, TTransport} class TSetIpAddressProcessor[I <: Iface]( iface: Iface) extends Processor[Iface](iface) with Logging { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/AbstractSession.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/AbstractSession.scala index a9e33f5a060..a00a12c1fb8 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/AbstractSession.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/AbstractSession.scala @@ -19,14 +19,13 @@ package org.apache.kyuubi.session import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_CLIENT_IP_KEY import org.apache.kyuubi.operation.{Operation, OperationHandle} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ abstract class AbstractSession( val protocol: TProtocolVersion, diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/Session.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/Session.scala index 2cdac9f3a78..c618c048093 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/Session.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/Session.scala @@ -17,10 +17,9 @@ package org.apache.kyuubi.session -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetInfoType, TGetInfoValue, TGetResultSetMetadataResp, TProtocolVersion} - import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.OperationHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetInfoType, TGetInfoValue, TGetResultSetMetadataResp, TProtocolVersion} trait Session { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionHandle.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionHandle.scala index d66999defe9..53b976884c3 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionHandle.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionHandle.scala @@ -19,9 +19,8 @@ package org.apache.kyuubi.session import java.util.UUID -import org.apache.hive.service.rpc.thrift.TSessionHandle - import org.apache.kyuubi.cli.Handle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TSessionHandle case class SessionHandle(identifier: UUID) { diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionManager.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionManager.scala index a83335102a8..5c71118f143 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionManager.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/session/SessionManager.scala @@ -25,14 +25,14 @@ import scala.collection.JavaConverters._ import scala.concurrent.duration.Duration import scala.util.control.NonFatal -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.operation.OperationManager import org.apache.kyuubi.service.CompositeService +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.util.ThreadUtils +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay /** * The [[SessionManager]] holds the all the connected [[Session]]s, provides us the APIs to @@ -90,7 +90,7 @@ abstract class SessionManager(name: String) extends CompositeService(name) { conf: Map[String, String]): Session protected def logSessionCountInfo(session: Session, action: String): Unit = { - info(s"${session.user}'s session with" + + info(s"${session.user}'s ${session.getClass.getSimpleName} with" + s" ${session.handle}${session.name.map("/" + _).getOrElse("")} is $action," + s" current opening sessions $getOpenSessionCount") } @@ -303,27 +303,33 @@ abstract class SessionManager(name: String) extends CompositeService(name) { val checkTask = new Runnable { override def run(): Unit = { + info(s"Checking sessions timeout, current count: $getOpenSessionCount") val current = System.currentTimeMillis if (!shutdown) { for (session <- handleToSession.values().asScala) { - if (session.lastAccessTime + session.sessionIdleTimeoutThreshold <= current && - session.getNoOperationTime > session.sessionIdleTimeoutThreshold) { - info(s"Closing session ${session.handle.identifier} that has been idle for more" + - s" than ${session.sessionIdleTimeoutThreshold} ms") - try { + try { + if (session.lastAccessTime + session.sessionIdleTimeoutThreshold <= current && + session.getNoOperationTime > session.sessionIdleTimeoutThreshold) { + info(s"Closing session ${session.handle.identifier} that has been idle for more" + + s" than ${session.sessionIdleTimeoutThreshold} ms") closeSession(session.handle) - } catch { - case NonFatal(e) => warn(s"Error closing idle session ${session.handle}", e) + } else { + session.closeExpiredOperations() } - } else { - session.closeExpiredOperations() + } catch { + case NonFatal(e) => warn(s"Error checking session ${session.handle} timeout", e) } } } } } - timeoutChecker.scheduleWithFixedDelay(checkTask, interval, interval, TimeUnit.MILLISECONDS) + scheduleTolerableRunnableWithFixedDelay( + timeoutChecker, + checkTask, + interval, + interval, + TimeUnit.MILLISECONDS) } private[kyuubi] def startTerminatingChecker(stop: () => Unit): Unit = if (!isServer) { @@ -341,7 +347,12 @@ abstract class SessionManager(name: String) extends CompositeService(name) { } } } - timeoutChecker.scheduleWithFixedDelay(checkTask, interval, interval, TimeUnit.MILLISECONDS) + scheduleTolerableRunnableWithFixedDelay( + timeoutChecker, + checkTask, + interval, + interval, + TimeUnit.MILLISECONDS) } } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/JdbcUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/JdbcUtils.scala index 996589cb742..4951004b671 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/JdbcUtils.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/JdbcUtils.scala @@ -98,6 +98,12 @@ object JdbcUtils extends Logging { } } + def mapResultSet[R](rs: ResultSet)(rowMapper: ResultSet => R): Seq[R] = { + val builder = Seq.newBuilder[R] + while (rs.next()) builder += rowMapper(rs) + builder.result + } + def redactPassword(password: Option[String]): String = { password match { case Some(s) if StringUtils.isNotBlank(s) => s"${"*" * s.length}(length:${s.length})" diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/RowSetUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/RowSetUtils.scala index f320fd90293..c79c2032740 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/RowSetUtils.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/RowSetUtils.scala @@ -17,15 +17,12 @@ package org.apache.kyuubi.util -import java.nio.ByteBuffer import java.time.{Instant, LocalDate, LocalDateTime, LocalTime, ZoneId} import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatterBuilder import java.time.temporal.ChronoField import java.util.{Date, Locale} -import scala.language.implicitConversions - import org.apache.commons.lang3.time.FastDateFormat private[kyuubi] object RowSetUtils { @@ -77,8 +74,4 @@ private[kyuubi] object RowSetUtils { timeZone.map(timestampFormatter.withZone(_).format(i)) .getOrElse(timestampFormatter.format(i)) } - - implicit def bitSetToBuffer(bitSet: java.util.BitSet): ByteBuffer = { - ByteBuffer.wrap(bitSet.toByteArray) - } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThreadUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThreadUtils.scala index 76d3f416f84..aeab37b6f1e 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThreadUtils.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThreadUtils.scala @@ -17,7 +17,7 @@ package org.apache.kyuubi.util -import java.util.concurrent.{Executors, ExecutorService, LinkedBlockingQueue, ScheduledExecutorService, ScheduledThreadPoolExecutor, ThreadPoolExecutor, TimeUnit} +import java.util.concurrent._ import scala.concurrent.Awaitable import scala.concurrent.duration.{Duration, FiniteDuration} @@ -109,4 +109,27 @@ object ThreadUtils extends Logging { thread.setUncaughtExceptionHandler(NamedThreadFactory.kyuubiUncaughtExceptionHandler) thread.start() } + + /** + * Schedule a runnable to the scheduled executor service. + * The exceptions thrown in the runnable will be caught and logged. + */ + def scheduleTolerableRunnableWithFixedDelay( + scheduler: ScheduledExecutorService, + runnable: Runnable, + initialDelay: Long, + delay: Long, + timeUnit: TimeUnit): Unit = { + scheduler.scheduleWithFixedDelay( + () => + try { + runnable.run() + } catch { + case t: Throwable => + error(s"Uncaught exception in thread ${Thread.currentThread().getName}", t) + }, + initialDelay, + delay, + timeUnit) + } } diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThriftUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThriftUtils.scala index bfe0bd64bc2..dbfbbc37d07 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThriftUtils.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ThriftUtils.scala @@ -17,9 +17,8 @@ package org.apache.kyuubi.util -import org.apache.hive.service.rpc.thrift.{TRow, TRowSet, TStatus, TStatusCode} - import org.apache.kyuubi.KyuubiSQLException +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TRow, TRowSet, TStatus, TStatusCode} object ThriftUtils { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/GlutenSuiteMixin.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/GlutenSuiteMixin.scala new file mode 100644 index 00000000000..6095e163017 --- /dev/null +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/GlutenSuiteMixin.scala @@ -0,0 +1,33 @@ +/* + * 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.kyuubi + +trait GlutenSuiteMixin { + protected def extraJars: String = { + System.getProperty("java.class.path") + .split(":") + .filter(_.contains("gluten-velox-bundle-spark")).head + } + + protected def extraConfigs: Map[String, String] = Map( + "spark.plugins" -> "io.glutenproject.GlutenPlugin", + "spark.memory.offHeap.size" -> "4g", + "spark.memory.offHeap.enabled" -> "true", + "spark.shuffle.manager" -> "org.apache.spark.shuffle.sort.ColumnarShuffleManager", + "spark.jars" -> extraJars) +} diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/HiveEngineTests.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/HiveEngineTests.scala index 028f755f6c8..29df9072423 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/HiveEngineTests.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/HiveEngineTests.scala @@ -23,6 +23,7 @@ import org.apache.commons.lang3.{JavaVersion, SystemUtils} import org.apache.kyuubi.operation.HiveJDBCTestHelper import org.apache.kyuubi.operation.meta.ResultSetSchemaConstant._ +import org.apache.kyuubi.util.JdbcUtils /** * hive tests disabled for JAVA 11 @@ -229,14 +230,12 @@ trait HiveEngineTests extends HiveJDBCTestHelper { assume(SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_1_8)) withJdbcStatement() { statement => val resultSet = statement.getConnection.getMetaData.getTableTypes - val expected = Set("TABLE", "VIEW", "MATERIALIZED_VIEW") - var tableTypes = Set[String]() - while (resultSet.next()) { - assert(expected.contains(resultSet.getString(TABLE_TYPE))) - tableTypes += resultSet.getString(TABLE_TYPE) - } - assert(!resultSet.next()) - assert(expected.size === tableTypes.size) + val hive2_1Expected = Set("TABLE", "VIEW", "INDEX_TABLE") + val hive2_3Expected = Set("TABLE", "VIEW", "MATERIALIZED_VIEW", "INDEX_TABLE") + val hive3Expected = Set("TABLE", "VIEW", "MATERIALIZED_VIEW") + val tableTypes = JdbcUtils.mapResultSet(resultSet) { rs => rs.getString(TABLE_TYPE) }.toSet + assert(tableTypes === hive2_1Expected || tableTypes === hive2_3Expected || + tableTypes === hive3Expected) } } @@ -387,10 +386,12 @@ trait HiveEngineTests extends HiveJDBCTestHelper { assert(typeInfo.getInt(DATA_TYPE) === java.sql.Types.TIMESTAMP) typeInfo.next() - assert(typeInfo.getString(TYPE_NAME) === "TIMESTAMP WITH LOCAL TIME ZONE") - assert(typeInfo.getInt(DATA_TYPE) === java.sql.Types.OTHER) + // Hive3 supports TIMESTAMP WITH LOCAL TIME ZONE + if (typeInfo.getString(TYPE_NAME) == "TIMESTAMP WITH LOCAL TIME ZONE") { + assert(typeInfo.getInt(DATA_TYPE) === java.sql.Types.OTHER) + typeInfo.next() + } - typeInfo.next() assert(typeInfo.getString(TYPE_NAME) === "INTERVAL_YEAR_MONTH") assert(typeInfo.getInt(DATA_TYPE) === java.sql.Types.OTHER) diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala index 4a099c71adf..0b1d65cc0ac 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi import java.lang.reflect.{InvocationTargetException, UndeclaredThrowableException} -import org.apache.hive.service.rpc.thrift.TStatusCode +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TStatusCode class KyuubiSQLExceptionSuite extends KyuubiFunSuite { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala index 4dbe6ea6711..71dc05f6176 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala @@ -25,6 +25,8 @@ import com.vladsch.flexmark.profile.pegdown.PegdownOptionsAdapter import com.vladsch.flexmark.util.data.{MutableDataHolder, MutableDataSet} import com.vladsch.flexmark.util.sequence.SequenceUtils.EOL +import org.apache.kyuubi.util.GoldenFileUtils.getLicenceContent + class MarkdownBuilder { private val buffer = new ListBuffer[String] @@ -58,24 +60,8 @@ class MarkdownBuilder { * @return */ def licence(): MarkdownBuilder = { - this ++= """ - | - |""" + buffer.appendAll(getLicenceContent(header = "")) + this } /** diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala index 5973fc6e7a6..60bdd3d22a6 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/UtilsSuite.scala @@ -23,12 +23,13 @@ import java.nio.file.{Files, Paths} import java.security.PrivilegedExceptionAction import java.util.Properties -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import org.apache.hadoop.security.UserGroupInformation import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN +import org.apache.kyuubi.util.command.CommandLineUtils._ class UtilsSuite extends KyuubiFunSuite { @@ -156,44 +157,40 @@ class UtilsSuite extends KyuubiFunSuite { val conf = new KyuubiConf() conf.set(SERVER_SECRET_REDACTION_PATTERN, "(?i)secret|password".r) - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += "main" - buffer += "--conf" - buffer += "kyuubi.my.password=sensitive_value" - buffer += "--conf" - buffer += "kyuubi.regular.property1=regular_value" - buffer += "--conf" - buffer += "kyuubi.my.secret=sensitive_value" - buffer += "--conf" - buffer += "kyuubi.regular.property2=regular_value" + buffer ++= confKeyValue("kyuubi.my.password", "sensitive_value") + buffer ++= confKeyValue("kyuubi.regular.property1", "regular_value") + buffer ++= confKeyValue("kyuubi.my.secret", "sensitive_value") + buffer ++= confKeyValue("kyuubi.regular.property2", "regular_value") - val commands = buffer.toArray + val commands = buffer // Redact sensitive information val redactedCmdArgs = Utils.redactCommandLineArgs(conf, commands) - val expectBuffer = new ArrayBuffer[String]() + val expectBuffer = new mutable.ListBuffer[String]() expectBuffer += "main" expectBuffer += "--conf" - expectBuffer += "kyuubi.my.password=" + Utils.REDACTION_REPLACEMENT_TEXT + expectBuffer += "kyuubi.my.password=" + REDACTION_REPLACEMENT_TEXT expectBuffer += "--conf" expectBuffer += "kyuubi.regular.property1=regular_value" expectBuffer += "--conf" - expectBuffer += "kyuubi.my.secret=" + Utils.REDACTION_REPLACEMENT_TEXT + expectBuffer += "kyuubi.my.secret=" + REDACTION_REPLACEMENT_TEXT expectBuffer += "--conf" expectBuffer += "kyuubi.regular.property2=regular_value" - assert(expectBuffer.toArray === redactedCmdArgs) + assert(expectBuffer === redactedCmdArgs) } test("redact sensitive information") { val secretKeys = Some("my.password".r) assert(Utils.redact(secretKeys, Seq(("kyuubi.my.password", "12345"))) === - Seq(("kyuubi.my.password", Utils.REDACTION_REPLACEMENT_TEXT))) + Seq(("kyuubi.my.password", REDACTION_REPLACEMENT_TEXT))) assert(Utils.redact(secretKeys, Seq(("anything", "kyuubi.my.password=12345"))) === - Seq(("anything", Utils.REDACTION_REPLACEMENT_TEXT))) + Seq(("anything", REDACTION_REPLACEMENT_TEXT))) assert(Utils.redact(secretKeys, Seq((999, "kyuubi.my.password=12345"))) === - Seq((999, Utils.REDACTION_REPLACEMENT_TEXT))) + Seq((999, REDACTION_REPLACEMENT_TEXT))) // Do not redact when value type is not string assert(Utils.redact(secretKeys, Seq(("my.password", 12345))) === Seq(("my.password", 12345))) diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/FetchOrientationSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/FetchOrientationSuite.scala index cfcd0b5c855..cfba4516cbb 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/FetchOrientationSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/FetchOrientationSuite.scala @@ -17,9 +17,8 @@ package org.apache.kyuubi.operation -import org.apache.hive.service.rpc.thrift.TFetchOrientation - import org.apache.kyuubi.KyuubiFunSuite +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TFetchOrientation class FetchOrientationSuite extends KyuubiFunSuite { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/HiveJDBCTestHelper.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/HiveJDBCTestHelper.scala index cbca415dc27..02cb9a00307 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/HiveJDBCTestHelper.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/HiveJDBCTestHelper.scala @@ -19,12 +19,12 @@ package org.apache.kyuubi.operation import java.sql.ResultSet -import org.apache.hive.service.rpc.thrift._ -import org.apache.hive.service.rpc.thrift.TCLIService.Iface -import org.apache.hive.service.rpc.thrift.TOperationState._ import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.Utils +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationState._ trait HiveJDBCTestHelper extends JDBCTestHelper { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperation.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperation.scala index c369e00efd8..df34577e01c 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperation.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperation.scala @@ -21,12 +21,11 @@ import java.nio.ByteBuffer import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TColumn, TColumnDesc, TFetchResultsResp, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TStringColumn, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TColumnDesc, TFetchResultsResp, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TStringColumn, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} import org.apache.kyuubi.util.ThriftUtils class NoopOperation(session: Session, shouldFail: Boolean = false) diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperationManager.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperationManager.scala index 352aae905ed..08fa9dd7cf1 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperationManager.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/NoopOperationManager.scala @@ -20,10 +20,9 @@ package org.apache.kyuubi.operation import java.nio.ByteBuffer import java.util -import org.apache.hive.service.rpc.thrift.{TColumn, TFetchResultsResp, TRow, TRowSet, TStatus, TStatusCode, TStringColumn} - import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TFetchResultsResp, TRow, TRowSet, TStatus, TStatusCode, TStringColumn} class NoopOperationManager extends OperationManager("noop") { private val invalid = "invalid" diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/OperationStateSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/OperationStateSuite.scala index 86c7e5e80a1..3052f7dcdee 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/OperationStateSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/OperationStateSuite.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.operation -import org.apache.hive.service.rpc.thrift.{TOperationState, TProtocolVersion} -import org.apache.hive.service.rpc.thrift.TOperationState._ - import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.operation.OperationState._ import org.apache.kyuubi.session.NoopSessionManager +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TOperationState, TProtocolVersion} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationState._ class OperationStateSuite extends KyuubiFunSuite { test("toTOperationState") { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkDataTypeTests.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkDataTypeTests.scala index 2709bc861f5..49f6b85d89f 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkDataTypeTests.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkDataTypeTests.scala @@ -18,6 +18,7 @@ package org.apache.kyuubi.operation import java.sql.{Date, Timestamp} +import java.util.Calendar import org.apache.kyuubi.util.SparkVersionUtil @@ -160,6 +161,23 @@ trait SparkDataTypeTests extends HiveJDBCTestHelper with SparkVersionUtil { } } + test("execute statement - select date with calendar") { + withJdbcStatement() { statement => + val resultSet = statement.executeQuery("SELECT DATE '2018-11-17' AS col") + assert(resultSet.next()) + assert(resultSet.getDate( + "col", + Calendar.getInstance()) === Date.valueOf("2018-11-17")) + assert(resultSet.getDate( + 1, + Calendar.getInstance()) === Date.valueOf("2018-11-17")) + val metaData = resultSet.getMetaData + assert(metaData.getColumnType(1) === java.sql.Types.DATE) + assert(metaData.getPrecision(1) === 10) + assert(metaData.getScale(1) === 0) + } + } + test("execute statement - select timestamp - second") { withJdbcStatement() { statement => val resultSet = statement.executeQuery( @@ -213,6 +231,26 @@ trait SparkDataTypeTests extends HiveJDBCTestHelper with SparkVersionUtil { } } + test("execute statement - select timestamp - second with calendar") { + withJdbcStatement() { statement => + val resultSet = statement.executeQuery( + "SELECT TIMESTAMP '2018-11-17 13:33:33' AS col") + assert(resultSet.next()) + assert(resultSet.getTimestamp( + "col", + Calendar.getInstance()) === Timestamp.valueOf( + "2018-11-17 13:33:33")) + assert(resultSet.getTimestamp( + 1, + Calendar.getInstance()) === Timestamp.valueOf( + "2018-11-17 13:33:33")) + val metaData = resultSet.getMetaData + assert(metaData.getColumnType(1) === java.sql.Types.TIMESTAMP) + assert(metaData.getPrecision(1) === 29) + assert(metaData.getScale(1) === 9) + } + } + test("execute statement - select daytime interval") { assume( resultFormat == "thrift" || diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala index 0ac56e3bcf0..b46b30402e8 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala @@ -24,10 +24,10 @@ import scala.collection.JavaConverters._ import org.apache.commons.io.FileUtils import org.apache.commons.lang3.StringUtils -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TGetResultSetMetadataReq, TOpenSessionReq, TStatusCode} import org.apache.kyuubi.{KYUUBI_VERSION, Utils} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TFetchResultsReq, TGetResultSetMetadataReq, TOpenSessionReq, TStatusCode} trait SparkQueryTests extends SparkDataTypeTests with HiveJDBCTestHelper { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/TClientTestUtils.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/TClientTestUtils.scala index d4b4ace88b2..7d2d6946467 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/TClientTestUtils.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/TClientTestUtils.scala @@ -22,15 +22,14 @@ import java.util.Base64 import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ -import org.apache.hive.service.rpc.thrift.TCLIService.Iface -import org.apache.thrift.protocol.TBinaryProtocol -import org.apache.thrift.transport.TSocket - import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.service.FrontendService import org.apache.kyuubi.service.authentication.PlainSASLHelper +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface +import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol +import org.apache.kyuubi.shaded.thrift.transport.TSocket object TClientTestUtils extends Logging { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/log/OperationLogSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/log/OperationLogSuite.scala index 570a8159bcf..d87ddec0b94 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/log/OperationLogSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/log/OperationLogSuite.scala @@ -23,12 +23,11 @@ import java.nio.file.{Files, Paths} import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TProtocolVersion, TRowSet} - import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle} import org.apache.kyuubi.session.NoopSessionManager +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TProtocolVersion, TRowSet} import org.apache.kyuubi.util.ThriftUtils class OperationLogSuite extends KyuubiFunSuite { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/TFrontendServiceSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/TFrontendServiceSuite.scala index 444bfe2cc3a..1fa6dc63a22 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/TFrontendServiceSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/TFrontendServiceSuite.scala @@ -21,7 +21,6 @@ import java.time.Duration import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ import org.scalatest.time._ import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException, Utils} @@ -30,6 +29,7 @@ import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.operation.{OperationHandle, TClientTestUtils} import org.apache.kyuubi.service.TFrontendService.FeServiceServerContext import org.apache.kyuubi.session.{AbstractSession, SessionHandle} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ class TFrontendServiceSuite extends KyuubiFunSuite { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala index 316c9b2dfdf..607c397d81f 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala @@ -20,11 +20,10 @@ package org.apache.kyuubi.service.authentication import java.security.Security import javax.security.auth.login.LoginException -import org.apache.thrift.transport.TSaslServerTransport - import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider +import org.apache.kyuubi.shaded.thrift.transport.TSaslServerTransport import org.apache.kyuubi.util.AssertionUtils._ import org.apache.kyuubi.util.KyuubiHadoopUtils diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala index d4290a2c6dd..2a55cc0d7ef 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala @@ -19,12 +19,11 @@ package org.apache.kyuubi.service.authentication import java.security.Security -import org.apache.thrift.transport.{TSaslServerTransport, TSocket} - import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.service.{NoopTBinaryFrontendServer, TBinaryFrontendService} import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider +import org.apache.kyuubi.shaded.thrift.transport.{TSaslServerTransport, TSocket} import org.apache.kyuubi.util.SemanticVersion class PlainSASLHelperSuite extends KyuubiFunSuite { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionImpl.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionImpl.scala index 91548a93e67..5c9e4802276 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionImpl.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionImpl.scala @@ -17,7 +17,7 @@ package org.apache.kyuubi.session -import org.apache.hive.service.rpc.thrift.TProtocolVersion +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class NoopSessionImpl( protocol: TProtocolVersion, diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionManager.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionManager.scala index 3a4088ed2da..075f2c2c373 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionManager.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/session/NoopSessionManager.scala @@ -17,10 +17,9 @@ package org.apache.kyuubi.session -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.operation.{NoopOperationManager, OperationManager} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class NoopSessionManager extends SessionManager("noop") { override val operationManager: OperationManager = new NoopOperationManager() diff --git a/kyuubi-ctl/pom.xml b/kyuubi-ctl/pom.xml index c453cd3af95..ad08f6ae7bb 100644 --- a/kyuubi-ctl/pom.xml +++ b/kyuubi-ctl/pom.xml @@ -50,7 +50,7 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} + ${kyuubi-relocated-zookeeper.artifacts} diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cli/AdminControlCliArguments.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cli/AdminControlCliArguments.scala index e015525b3aa..5a45630c685 100644 --- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cli/AdminControlCliArguments.scala +++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cli/AdminControlCliArguments.scala @@ -61,7 +61,6 @@ class AdminControlCliArguments(args: Seq[String], env: Map[String, String] = sys | type ${cliConfig.engineOpts.engineType} | sharelevel ${cliConfig.engineOpts.engineShareLevel} | sharesubdomain ${cliConfig.engineOpts.engineSubdomain} - | all ${cliConfig.engineOpts.all} """.stripMargin case ControlObject.SERVER => s"""Parsed arguments: diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/delete/DeleteBatchCommand.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/delete/DeleteBatchCommand.scala index 3988620adb8..ee4a14d2666 100644 --- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/delete/DeleteBatchCommand.scala +++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/delete/DeleteBatchCommand.scala @@ -36,7 +36,7 @@ class DeleteBatchCommand(cliConfig: CliConfig) extends Command[Batch](cliConfig) val batchRestApi: BatchRestApi = new BatchRestApi(kyuubiRestClient) val batchId = normalizedCliConfig.batchOpts.batchId - val result = batchRestApi.deleteBatch(batchId, normalizedCliConfig.commonOpts.hs2ProxyUser) + val result = batchRestApi.deleteBatch(batchId) info(JsonUtils.toJson(result)) diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/list/AdminListEngineCommand.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/list/AdminListEngineCommand.scala index 96be5cc4744..acd6fe44416 100644 --- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/list/AdminListEngineCommand.scala +++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/list/AdminListEngineCommand.scala @@ -38,8 +38,7 @@ class AdminListEngineCommand(cliConfig: CliConfig) normalizedCliConfig.engineOpts.engineType, normalizedCliConfig.engineOpts.engineShareLevel, normalizedCliConfig.engineOpts.engineSubdomain, - normalizedCliConfig.commonOpts.hs2ProxyUser, - normalizedCliConfig.engineOpts.all).asScala + normalizedCliConfig.commonOpts.hs2ProxyUser).asScala } } diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala index c02826b6875..c7e367405e8 100644 --- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala +++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala @@ -52,7 +52,7 @@ object AdminCommandLine extends CommonCommandLine { .text("\tDelete resources.") .action((_, c) => c.copy(action = ControlAction.DELETE)) .children( - deleteEngineCmd(builder).text("\tDelete the specified engine node for user."))) + engineCmd(builder).text("\tDelete the specified engine node for user."))) } @@ -64,7 +64,7 @@ object AdminCommandLine extends CommonCommandLine { .text("\tList information about resources.") .action((_, c) => c.copy(action = ControlAction.LIST)) .children( - listEngineCmd(builder).text("\tList the engine nodes"), + engineCmd(builder).text("\tList all the engine nodes for a user"), serverCmd(builder).text("\tList all the server nodes"))) } @@ -80,7 +80,7 @@ object AdminCommandLine extends CommonCommandLine { refreshConfigCmd(builder).text("\tRefresh the config with specified type."))) } - private def deleteEngineCmd(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] = { + private def engineCmd(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] = { import builder._ cmd("engine").action((_, c) => c.copy(resource = ControlObject.ENGINE)) .children( @@ -95,24 +95,6 @@ object AdminCommandLine extends CommonCommandLine { .text("The engine share level this engine belong to.")) } - private def listEngineCmd(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] = { - import builder._ - cmd("engine").action((_, c) => c.copy(resource = ControlObject.ENGINE)) - .children( - opt[String]("engine-type").abbr("et") - .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(engineType = v))) - .text("The engine type this engine belong to."), - opt[String]("engine-subdomain").abbr("es") - .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(engineSubdomain = v))) - .text("The engine subdomain this engine belong to."), - opt[String]("engine-share-level").abbr("esl") - .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(engineShareLevel = v))) - .text("The engine share level this engine belong to."), - opt[String]("all").abbr("a") - .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(all = v))) - .text("All the engine.")) - } - private def serverCmd(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] = { import builder._ cmd("server").action((_, c) => c.copy(resource = ControlObject.SERVER)) diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/CliConfig.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/CliConfig.scala index 4ccae109c6a..7818f694a3f 100644 --- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/CliConfig.scala +++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/CliConfig.scala @@ -77,7 +77,6 @@ case class EngineOpts( user: String = null, engineType: String = null, engineSubdomain: String = null, - engineShareLevel: String = null, - all: String = null) + engineShareLevel: String = null) case class AdminConfigOpts(configType: String = null) diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala index ae7c0fa1b96..52a2796f463 100644 --- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala +++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala @@ -158,14 +158,13 @@ class AdminControlCliArgumentsSuite extends KyuubiFunSuite with TestPrematureExi |Command: list [engine|server] | List information about resources. |Command: list engine [options] - | List the engine nodes + | List all the engine nodes for a user | -et, --engine-type | The engine type this engine belong to. | -es, --engine-subdomain | The engine subdomain this engine belong to. | -esl, --engine-share-level | The engine share level this engine belong to. - | -a, --all All the engine. |Command: list server | List all the server nodes | diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/BatchCliArgumentsSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/BatchCliArgumentsSuite.scala index bf8f101e00a..5987ac16338 100644 --- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/BatchCliArgumentsSuite.scala +++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/BatchCliArgumentsSuite.scala @@ -119,18 +119,6 @@ class BatchCliArgumentsSuite extends KyuubiFunSuite with TestPrematureExit { } } - test("delete batch with hs2ProxyUser") { - val args = Array( - "delete", - "batch", - "f7fd702c-e54e-11ec-8fea-0242ac120002", - "--hs2ProxyUser", - "b_user") - val opArgs = new ControlCliArguments(args) - assert(opArgs.cliConfig.batchOpts.batchId == "f7fd702c-e54e-11ec-8fea-0242ac120002") - assert(opArgs.cliConfig.commonOpts.hs2ProxyUser == "b_user") - } - test("test list batch option") { val args = Array( "list", diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala index 43a694a081a..b0319e497ed 100644 --- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala +++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala @@ -190,9 +190,9 @@ class ControlCliSuite extends KyuubiFunSuite with TestPrematureExit { assert(children.size == 2) assert(children.head.startsWith( - s"serviceUri=localhost:10000;version=$KYUUBI_VERSION;sequence=")) + s"serverUri=localhost:10000;version=$KYUUBI_VERSION;sequence=")) assert(children.last.startsWith( - s"serviceUri=localhost:10001;version=$KYUUBI_VERSION;sequence=")) + s"serverUri=localhost:10001;version=$KYUUBI_VERSION;sequence=")) children.foreach { child => framework.delete(s"""$znodeRoot/$child""") } diff --git a/kyuubi-ha/pom.xml b/kyuubi-ha/pom.xml index 129f7a53dbb..f007f2c7064 100644 --- a/kyuubi-ha/pom.xml +++ b/kyuubi-ha/pom.xml @@ -39,7 +39,7 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} + ${kyuubi-relocated-zookeeper.artifacts} diff --git a/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/etcd/EtcdDiscoveryClient.scala b/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/etcd/EtcdDiscoveryClient.scala index d979804f417..7edc7e8a310 100644 --- a/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/etcd/EtcdDiscoveryClient.scala +++ b/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/etcd/EtcdDiscoveryClient.scala @@ -335,7 +335,7 @@ class EtcdDiscoveryClient(conf: KyuubiConf) extends DiscoveryClient { val extraInfo = attributes.map(kv => kv._1 + "=" + kv._2).mkString(";", ";", "") val pathPrefix = DiscoveryPaths.makePath( namespace, - s"serviceUri=$instance;version=${version.getOrElse(KYUUBI_VERSION)}" + + s"serverUri=$instance;version=${version.getOrElse(KYUUBI_VERSION)}" + s"${extraInfo.stripSuffix(";")};${session}sequence=") val znode = instance diff --git a/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClient.scala b/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClient.scala index 2db7d89d649..a06087d3adf 100644 --- a/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClient.scala +++ b/kyuubi-ha/src/main/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClient.scala @@ -361,7 +361,7 @@ class ZookeeperDiscoveryClient(conf: KyuubiConf) extends DiscoveryClient { val extraInfo = attributes.map(kv => kv._1 + "=" + kv._2).mkString(";", ";", "") val pathPrefix = ZKPaths.makePath( namespace, - s"serviceUri=$instance;version=${version.getOrElse(KYUUBI_VERSION)}" + + s"serverUri=$instance;version=${version.getOrElse(KYUUBI_VERSION)}" + s"${extraInfo.stripSuffix(";")};${session}sequence=") var localServiceNode: PersistentNode = null val createMode = diff --git a/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/DiscoveryClientTests.scala b/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/DiscoveryClientTests.scala index 9caf3864640..53c0586f5a6 100644 --- a/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/DiscoveryClientTests.scala +++ b/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/DiscoveryClientTests.scala @@ -60,7 +60,7 @@ trait DiscoveryClientTests extends KyuubiFunSuite { assert(discoveryClient.pathExists(basePath)) val children = discoveryClient.getChildren(basePath) assert(children.head === - s"serviceUri=${service.frontendServices.head.connectionUrl};" + + s"serverUri=${service.frontendServices.head.connectionUrl};" + s"version=$KYUUBI_VERSION;sequence=0000000000") children.foreach { child => @@ -107,7 +107,7 @@ trait DiscoveryClientTests extends KyuubiFunSuite { assert(discoveryClient.pathExists(basePath)) val children = discoveryClient.getChildren(basePath) assert(children.head === - s"serviceUri=${service.frontendServices.head.connectionUrl};" + + s"serverUri=${service.frontendServices.head.connectionUrl};" + s"version=$KYUUBI_VERSION;sequence=0000000000") children.foreach { child => diff --git a/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClientSuite.scala b/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClientSuite.scala index dd78e1fb8a0..34ed0559383 100644 --- a/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClientSuite.scala +++ b/kyuubi-ha/src/test/scala/org/apache/kyuubi/ha/client/zookeeper/ZookeeperDiscoveryClientSuite.scala @@ -196,7 +196,7 @@ abstract class ZookeeperDiscoveryClientSuite extends DiscoveryClientTests assert(discoveryClient.pathExists(basePath)) val children = discoveryClient.getChildren(basePath) assert(children.head === - s"serviceUri=${service.frontendServices.head.connectionUrl};" + + s"serverUri=${service.frontendServices.head.connectionUrl};" + s"version=$KYUUBI_VERSION;sequence=0000000000") children.foreach { child => diff --git a/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/KyuubiBeeLine.java b/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/KyuubiBeeLine.java index 224cbb3ce11..c786da35f24 100644 --- a/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/KyuubiBeeLine.java +++ b/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/KyuubiBeeLine.java @@ -85,7 +85,7 @@ public KyuubiBeeLine(boolean isBeeLine) { @Override void usage() { super.usage(); - output("Usage: java \" + KyuubiBeeLine.class.getCanonicalName()"); + output("Usage: java " + KyuubiBeeLine.class.getCanonicalName()); output(" --python-mode Execute python code/script."); } diff --git a/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/logs/KyuubiBeelineInPlaceUpdateStream.java b/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/logs/KyuubiBeelineInPlaceUpdateStream.java index afe777f502f..e0087332e12 100644 --- a/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/logs/KyuubiBeelineInPlaceUpdateStream.java +++ b/kyuubi-hive-beeline/src/main/java/org/apache/hive/beeline/logs/KyuubiBeelineInPlaceUpdateStream.java @@ -21,9 +21,9 @@ import java.util.List; import org.apache.hadoop.hive.common.log.InPlaceUpdate; import org.apache.hadoop.hive.common.log.ProgressMonitor; -import org.apache.hive.service.rpc.thrift.TJobExecutionStatus; -import org.apache.hive.service.rpc.thrift.TProgressUpdateResp; import org.apache.kyuubi.jdbc.hive.logs.InPlaceUpdateStream; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TJobExecutionStatus; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProgressUpdateResp; public class KyuubiBeelineInPlaceUpdateStream implements InPlaceUpdateStream { private InPlaceUpdate inPlaceUpdate; diff --git a/kyuubi-hive-jdbc-shaded/pom.xml b/kyuubi-hive-jdbc-shaded/pom.xml index 174f199bead..ba2938a4d36 100644 --- a/kyuubi-hive-jdbc-shaded/pom.xml +++ b/kyuubi-hive-jdbc-shaded/pom.xml @@ -84,10 +84,6 @@ codegen ${kyuubi.shade.packageName}.codegen - - com.facebook - ${kyuubi.shade.packageName}.com.facebook - com.google ${kyuubi.shade.packageName}.com.google @@ -108,18 +104,10 @@ org.apache.commons ${kyuubi.shade.packageName}.org.apache.commons - - org.apache.hive - ${kyuubi.shade.packageName}.org.apache.hive - org.apache.http ${kyuubi.shade.packageName}.org.apache.http - - org.apache.thrift - ${kyuubi.shade.packageName}.org.apache.thrift - diff --git a/kyuubi-hive-jdbc/pom.xml b/kyuubi-hive-jdbc/pom.xml index aa5e7c161d5..1ec5e597fa0 100644 --- a/kyuubi-hive-jdbc/pom.xml +++ b/kyuubi-hive-jdbc/pom.xml @@ -71,21 +71,6 @@ jackson-datatype-jsr310 - - org.apache.thrift - libfb303 - - - - org.apache.thrift - libthrift - - - - org.apache.hive - hive-service-rpc - - commons-codec commons-codec @@ -114,7 +99,12 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} + kyuubi-relocated-hive-service-rpc + + + + org.apache.kyuubi + ${kyuubi-relocated-zookeeper.artifacts} diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcColumn.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcColumn.java index 365fc1d3e27..a6c4a948b7b 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcColumn.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcColumn.java @@ -23,10 +23,10 @@ import java.sql.Date; import java.sql.SQLException; import java.sql.Timestamp; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalDayTime; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalYearMonth; import org.apache.kyuubi.jdbc.hive.common.TimestampTZ; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; /** Column metadata. */ public class JdbcColumn { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcConnectionParams.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcConnectionParams.java index 71949b9dfea..c60f3489958 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcConnectionParams.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/JdbcConnectionParams.java @@ -33,6 +33,7 @@ public class JdbcConnectionParams { // Client param names: + static final String CLIENT_PROTOCOL_VERSION = "clientProtocolVersion"; // Retry setting static final String RETRIES = "retries"; @@ -48,6 +49,7 @@ public class JdbcConnectionParams { public static final String AUTH_KYUUBI_SERVER_PRINCIPAL = "kyuubiServerPrincipal"; public static final String AUTH_KYUUBI_CLIENT_PRINCIPAL = "kyuubiClientPrincipal"; public static final String AUTH_KYUUBI_CLIENT_KEYTAB = "kyuubiClientKeytab"; + public static final String AUTH_KYUUBI_CLIENT_TICKET_CACHE = "kyuubiClientTicketCache"; public static final String AUTH_PASSWD = "password"; public static final String AUTH_KERBEROS_AUTH_TYPE = "kerberosAuthType"; public static final String AUTH_KERBEROS_AUTH_TYPE_FROM_SUBJECT = "fromSubject"; @@ -114,6 +116,8 @@ public class JdbcConnectionParams { // Currently supports JKS keystore format static final String SSL_TRUST_STORE_TYPE = "JKS"; + static final String SSL_STORE_PASSWORD_PATH = "storePasswordPath"; + static final String HIVE_VAR_PREFIX = "hivevar:"; static final String HIVE_CONF_PREFIX = "hiveconf:"; private String host = null; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowBasedResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowBasedResultSet.java index ef5008503aa..52f178a2254 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowBasedResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowBasedResultSet.java @@ -23,15 +23,16 @@ import java.math.MathContext; import java.nio.charset.StandardCharsets; import java.sql.*; +import java.util.Calendar; import java.util.List; import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.vector.VectorSchemaRoot; import org.apache.arrow.vector.types.pojo.Schema; -import org.apache.hive.service.rpc.thrift.TTableSchema; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.adapter.SQLResultSet; import org.apache.kyuubi.jdbc.hive.arrow.ArrowColumnarBatchRow; import org.apache.kyuubi.jdbc.hive.arrow.ArrowUtils; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTableSchema; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; /** Data independent base class which implements the common part of all Kyuubi result sets. */ @SuppressWarnings("deprecation") @@ -198,6 +199,32 @@ public Date getDate(String columnName) throws SQLException { return getDate(findColumn(columnName)); } + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + Date value = getDate(columnIndex); + if (value == null) { + return null; + } + try { + return parseDate(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException("Cannot convert column " + columnIndex + " to date: " + e, e); + } + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return this.getDate(findColumn(columnLabel), cal); + } + + private Date parseDate(Date value, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + cal.setTime(value); + return new Date(cal.getTimeInMillis()); + } + @Override public double getDouble(int columnIndex) throws SQLException { try { @@ -406,6 +433,83 @@ public Timestamp getTimestamp(String columnName) throws SQLException { return getTimestamp(findColumn(columnName)); } + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + Timestamp value = getTimestamp(columnIndex); + if (value == null) { + return null; + } + try { + return parseTimestamp(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException( + "Cannot convert column " + columnIndex + " to timestamp: " + e, e); + } + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return this.getTimestamp(findColumn(columnLabel), cal); + } + + private Timestamp parseTimestamp(Timestamp timestamp, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + long v = timestamp.getTime(); + cal.setTimeInMillis(v); + timestamp = new Timestamp(cal.getTime().getTime()); + return timestamp; + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + Object obj = getObject(columnIndex); + if (obj == null) { + return null; + } + if (obj instanceof Time) { + return (Time) obj; + } + if (obj instanceof String) { + return Time.valueOf((String) obj); + } + throw new KyuubiSQLException("Illegal conversion"); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(findColumn(columnLabel)); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + Time value = getTime(columnIndex); + if (value == null) { + return null; + } + try { + return parseTime(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException("Cannot convert column " + columnIndex + " to time: " + e, e); + } + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return this.getTime(findColumn(columnLabel), cal); + } + + private Time parseTime(Time date, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + long v = date.getTime(); + cal.setTimeInMillis(v); + date = new Time(cal.getTime().getTime()); + return date; + } + @Override public int getType() throws SQLException { return ResultSet.TYPE_FORWARD_ONLY; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowQueryResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowQueryResultSet.java index 54491b2d670..163322ccb32 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowQueryResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiArrowQueryResultSet.java @@ -31,12 +31,12 @@ import org.apache.arrow.vector.ipc.ReadChannel; import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; import org.apache.arrow.vector.ipc.message.MessageSerializer; -import org.apache.hive.service.rpc.thrift.*; import org.apache.kyuubi.jdbc.hive.arrow.ArrowColumnVector; import org.apache.kyuubi.jdbc.hive.arrow.ArrowColumnarBatch; import org.apache.kyuubi.jdbc.hive.arrow.ArrowColumnarBatchRow; import org.apache.kyuubi.jdbc.hive.arrow.ArrowUtils; import org.apache.kyuubi.jdbc.hive.common.HiveDecimal; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiBaseResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiBaseResultSet.java index a9d32e8cafb..cf47e104295 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiBaseResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiBaseResultSet.java @@ -23,13 +23,14 @@ import java.math.MathContext; import java.nio.charset.StandardCharsets; import java.sql.*; +import java.util.Calendar; import java.util.List; -import org.apache.hive.service.rpc.thrift.TTableSchema; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.adapter.SQLResultSet; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalDayTime; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalYearMonth; import org.apache.kyuubi.jdbc.hive.common.TimestampTZUtil; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTableSchema; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; /** Data independent base class which implements the common part of all Kyuubi result sets. */ @SuppressWarnings("deprecation") @@ -182,6 +183,32 @@ public Date getDate(String columnName) throws SQLException { return getDate(findColumn(columnName)); } + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + Date value = getDate(columnIndex); + if (value == null) { + return null; + } + try { + return parseDate(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException("Cannot convert column " + columnIndex + " to date: " + e, e); + } + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return this.getDate(findColumn(columnLabel), cal); + } + + private Date parseDate(Date value, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + cal.setTime(value); + return new Date(cal.getTimeInMillis()); + } + @Override public double getDouble(int columnIndex) throws SQLException { try { @@ -412,6 +439,83 @@ public Timestamp getTimestamp(String columnName) throws SQLException { return getTimestamp(findColumn(columnName)); } + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + Timestamp value = getTimestamp(columnIndex); + if (value == null) { + return null; + } + try { + return parseTimestamp(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException( + "Cannot convert column " + columnIndex + " to timestamp: " + e, e); + } + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return this.getTimestamp(findColumn(columnLabel), cal); + } + + private Timestamp parseTimestamp(Timestamp timestamp, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + long v = timestamp.getTime(); + cal.setTimeInMillis(v); + timestamp = new Timestamp(cal.getTime().getTime()); + return timestamp; + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + Object obj = getObject(columnIndex); + if (obj == null) { + return null; + } + if (obj instanceof Time) { + return (Time) obj; + } + if (obj instanceof String) { + return Time.valueOf((String) obj); + } + throw new KyuubiSQLException("Illegal conversion"); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(findColumn(columnLabel)); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + Time value = getTime(columnIndex); + if (value == null) { + return null; + } + try { + return parseTime(value, cal); + } catch (IllegalArgumentException e) { + throw new KyuubiSQLException("Cannot convert column " + columnIndex + " to time: " + e, e); + } + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return this.getTime(findColumn(columnLabel), cal); + } + + private Time parseTime(Time date, Calendar cal) { + if (cal == null) { + cal = Calendar.getInstance(); + } + long v = date.getTime(); + cal.setTimeInMillis(v); + date = new Time(cal.getTime().getTime()); + return date; + } + @Override public int getType() throws SQLException { return ResultSet.TYPE_FORWARD_ONLY; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java index c23985328ec..47de5f7480b 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java @@ -42,7 +42,6 @@ import javax.security.sasl.Sasl; import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.hive.service.rpc.thrift.*; import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponse; import org.apache.http.NoHttpResponseException; @@ -66,11 +65,12 @@ import org.apache.kyuubi.jdbc.hive.cli.RowSet; import org.apache.kyuubi.jdbc.hive.cli.RowSetFactory; import org.apache.kyuubi.jdbc.hive.logs.KyuubiLoggable; -import org.apache.thrift.TException; -import org.apache.thrift.protocol.TBinaryProtocol; -import org.apache.thrift.transport.THttpClient; -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.thrift.TException; +import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol; +import org.apache.kyuubi.shaded.thrift.transport.THttpClient; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -539,7 +539,8 @@ public long getRetryInterval() { if (useSsl) { String useTwoWaySSL = sessConfMap.get(USE_TWO_WAY_SSL); String sslTrustStorePath = sessConfMap.get(SSL_TRUST_STORE); - String sslTrustStorePassword = sessConfMap.get(SSL_TRUST_STORE_PASSWORD); + String sslTrustStorePassword = + Utils.getPassword(sessConfMap, JdbcConnectionParams.SSL_TRUST_STORE_PASSWORD); KeyStore sslTrustStore; SSLConnectionSocketFactory socketFactory; SSLContext sslContext; @@ -559,7 +560,8 @@ public long getRetryInterval() { // Pick trust store config from the given path sslTrustStore = KeyStore.getInstance(SSL_TRUST_STORE_TYPE); try (FileInputStream fis = new FileInputStream(sslTrustStorePath)) { - sslTrustStore.load(fis, sslTrustStorePassword.toCharArray()); + sslTrustStore.load( + fis, sslTrustStorePassword != null ? sslTrustStorePassword.toCharArray() : null); } sslContext = SSLContexts.custom().loadTrustMaterial(sslTrustStore, null).build(); socketFactory = @@ -590,7 +592,8 @@ private TTransport createUnderlyingTransport() throws TTransportException { if (isSslConnection()) { // get SSL socket String sslTrustStore = sessConfMap.get(SSL_TRUST_STORE); - String sslTrustStorePassword = sessConfMap.get(SSL_TRUST_STORE_PASSWORD); + String sslTrustStorePassword = + Utils.getPassword(sessConfMap, JdbcConnectionParams.SSL_TRUST_STORE_PASSWORD); if (sslTrustStore == null || sslTrustStore.isEmpty()) { transport = ThriftUtils.getSSLSocket(host, port, connectTimeout, socketTimeout); @@ -661,7 +664,8 @@ SSLConnectionSocketFactory getTwoWaySSLSocketFactory() throws SQLException { KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SUNX509_ALGORITHM_STRING, SUNJSSE_ALGORITHM_STRING); String keyStorePath = sessConfMap.get(SSL_KEY_STORE); - String keyStorePassword = sessConfMap.get(SSL_KEY_STORE_PASSWORD); + String keyStorePassword = + Utils.getPassword(sessConfMap, JdbcConnectionParams.SSL_KEY_STORE_PASSWORD); KeyStore sslKeyStore = KeyStore.getInstance(SSL_KEY_STORE_TYPE); if (keyStorePath == null || keyStorePath.isEmpty()) { @@ -677,7 +681,8 @@ SSLConnectionSocketFactory getTwoWaySSLSocketFactory() throws SQLException { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(SUNX509_ALGORITHM_STRING); String trustStorePath = sessConfMap.get(SSL_TRUST_STORE); - String trustStorePassword = sessConfMap.get(SSL_TRUST_STORE_PASSWORD); + String trustStorePassword = + Utils.getPassword(sessConfMap, JdbcConnectionParams.SSL_TRUST_STORE_PASSWORD); KeyStore sslTrustStore = KeyStore.getInstance(SSL_TRUST_STORE_TYPE); if (trustStorePath == null || trustStorePath.isEmpty()) { @@ -685,7 +690,8 @@ SSLConnectionSocketFactory getTwoWaySSLSocketFactory() throws SQLException { SSL_TRUST_STORE + " Not configured for 2 way SSL connection"); } try (FileInputStream fis = new FileInputStream(trustStorePath)) { - sslTrustStore.load(fis, trustStorePassword.toCharArray()); + sslTrustStore.load( + fis, trustStorePassword != null ? trustStorePassword.toCharArray() : null); } trustManagerFactory.init(sslTrustStore); SSLContext context = SSLContext.getInstance("TLS"); @@ -733,6 +739,18 @@ private void openSession() throws SQLException { if (sessVars.containsKey(HS2_PROXY_USER)) { openConf.put(HS2_PROXY_USER, sessVars.get(HS2_PROXY_USER)); } + String clientProtocolStr = + sessVars.getOrDefault( + CLIENT_PROTOCOL_VERSION, openReq.getClient_protocol().getValue() + ""); + TProtocolVersion clientProtocol = + TProtocolVersion.findByValue(Integer.parseInt(clientProtocolStr)); + if (clientProtocol == null) { + throw new IllegalArgumentException( + String.format( + "Unsupported Hive2 protocol version %s specified by session conf key %s", + clientProtocolStr, CLIENT_PROTOCOL_VERSION)); + } + openReq.setClient_protocol(clientProtocol); try { openConf.put("kyuubi.client.ipAddress", InetAddress.getLocalHost().getHostAddress()); } catch (UnknownHostException e) { @@ -870,7 +888,8 @@ private Subject createSubject() { AccessControlContext context = AccessController.getContext(); return Subject.getSubject(context); } else if (isTgtCacheAuthMode()) { - return KerberosAuthenticationManager.getTgtCacheAuthentication().getSubject(); + String ticketCache = sessConfMap.get(AUTH_KYUUBI_CLIENT_TICKET_CACHE); + return KerberosAuthenticationManager.getTgtCacheAuthentication(ticketCache).getSubject(); } else { // This should never happen throw new IllegalArgumentException("Unsupported auth mode"); diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiDatabaseMetaData.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiDatabaseMetaData.java index c6ab3a277c4..cb32bbd8d46 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiDatabaseMetaData.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiDatabaseMetaData.java @@ -17,7 +17,7 @@ package org.apache.kyuubi.jdbc.hive; -import static org.apache.hive.service.rpc.thrift.TTypeId.*; +import static org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId.*; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -28,10 +28,10 @@ import java.util.Comparator; import java.util.List; import java.util.jar.Attributes; -import org.apache.hive.service.rpc.thrift.*; import org.apache.kyuubi.jdbc.KyuubiHiveDriver; import org.apache.kyuubi.jdbc.hive.adapter.SQLDatabaseMetaData; -import org.apache.thrift.TException; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.thrift.TException; /** KyuubiDatabaseMetaData. */ public class KyuubiDatabaseMetaData implements SQLDatabaseMetaData { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiMetaDataResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiMetaDataResultSet.java index 48fdaaa1a68..b8c865dd1ce 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiMetaDataResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiMetaDataResultSet.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.hive.service.rpc.thrift.TTypeId; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; public abstract class KyuubiMetaDataResultSet extends KyuubiBaseResultSet { protected List data = Collections.emptyList(); diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiPreparedStatement.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiPreparedStatement.java index 1e53f940157..d8105ea07d3 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiPreparedStatement.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiPreparedStatement.java @@ -28,9 +28,9 @@ import java.text.MessageFormat; import java.util.HashMap; import java.util.Scanner; -import org.apache.hive.service.rpc.thrift.TCLIService; -import org.apache.hive.service.rpc.thrift.TSessionHandle; import org.apache.kyuubi.jdbc.hive.adapter.SQLPreparedStatement; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TSessionHandle; /** KyuubiPreparedStatement. */ public class KyuubiPreparedStatement extends KyuubiStatement implements SQLPreparedStatement { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiQueryResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiQueryResultSet.java index 242ec772021..81873f0dcb2 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiQueryResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiQueryResultSet.java @@ -22,11 +22,11 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.locks.ReentrantLock; -import org.apache.hive.service.rpc.thrift.*; import org.apache.kyuubi.jdbc.hive.cli.RowSet; import org.apache.kyuubi.jdbc.hive.cli.RowSetFactory; import org.apache.kyuubi.jdbc.hive.common.HiveDecimal; -import org.apache.thrift.TException; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.thrift.TException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiResultSetMetaData.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiResultSetMetaData.java index bec1ca7fd32..fa121d2871b 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiResultSetMetaData.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiResultSetMetaData.java @@ -20,8 +20,8 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.List; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.adapter.SQLResultSetMetaData; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; /** KyuubiResultSetMetaData. */ public class KyuubiResultSetMetaData implements SQLResultSetMetaData { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java index 7d26f807898..b5e6579379b 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java @@ -20,7 +20,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import org.apache.hive.service.rpc.thrift.TStatus; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TStatus; import org.apache.kyuubi.util.reflect.DynConstructors; public class KyuubiSQLException extends SQLException { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiStatement.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiStatement.java index cbe32eca65e..346c8a964e1 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiStatement.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiStatement.java @@ -21,14 +21,14 @@ import java.sql.*; import java.util.*; import org.apache.commons.lang3.StringUtils; -import org.apache.hive.service.rpc.thrift.*; import org.apache.kyuubi.jdbc.hive.adapter.SQLStatement; import org.apache.kyuubi.jdbc.hive.cli.FetchType; import org.apache.kyuubi.jdbc.hive.cli.RowSet; import org.apache.kyuubi.jdbc.hive.cli.RowSetFactory; import org.apache.kyuubi.jdbc.hive.logs.InPlaceUpdateStream; import org.apache.kyuubi.jdbc.hive.logs.KyuubiLoggable; -import org.apache.thrift.TException; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.thrift.TException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/Utils.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/Utils.java index d0167e3e490..2a0462aeda2 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/Utils.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/Utils.java @@ -28,8 +28,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; -import org.apache.hive.service.rpc.thrift.TStatus; -import org.apache.hive.service.rpc.thrift.TStatusCode; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TStatus; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TStatusCode; +import org.apache.kyuubi.util.reflect.DynConstructors; +import org.apache.kyuubi.util.reflect.DynMethods; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +60,11 @@ public class Utils { public static final String HIVE_SERVER2_RETRY_TRUE = "true"; public static final String HIVE_SERVER2_RETRY_FALSE = "false"; + public static final String HADOOP_CONFIGURATION_CLASS = "org.apache.hadoop.conf.Configuration"; + + public static final String HADOOP_SECURITY_CREDENTIAL_PATH = + "hadoop.security.credential.provider.path"; + public static final Pattern KYUUBI_OPERATION_HINT_PATTERN = Pattern.compile("^__kyuubi_operation_result_(.*)__=(.*)", Pattern.CASE_INSENSITIVE); @@ -201,7 +208,7 @@ public static JdbcConnectionParams extractURLComponents(String uri, Properties i uri = uri.replace(urlPrefix, urlPrefix + authorityFromClientJdbcURL); } connParams.setSuppliedURLAuthority(authorityFromClientJdbcURL); - uri = uri.replace(authorityFromClientJdbcURL, dummyAuthorityString); + uri = uri.replaceFirst(authorityFromClientJdbcURL, dummyAuthorityString); // Now parse the connection uri with dummy authority URI jdbcURI = URI.create(uri.substring(URI_JDBC_PREFIX.length())); @@ -292,6 +299,13 @@ public static JdbcConnectionParams extractURLComponents(String uri, Properties i } } } + if (!connParams.getSessionVars().containsKey(CLIENT_PROTOCOL_VERSION)) { + if (info.containsKey(CLIENT_PROTOCOL_VERSION)) { + connParams + .getSessionVars() + .put(CLIENT_PROTOCOL_VERSION, info.getProperty(CLIENT_PROTOCOL_VERSION)); + } + } // Extract user/password from JDBC connection properties if its not supplied // in the connection URL if (!connParams.getSessionVars().containsKey(AUTH_USER)) { @@ -563,4 +577,69 @@ public static synchronized String getVersion() { } return KYUUBI_CLIENT_VERSION; } + + /** + * Method to get the password from the credential provider + * + * @param configuration Hadoop configuration + * @param providerPath provider path + * @param key alias name + * @return password + */ + private static String getPasswordFromCredentialProvider( + Object configuration, String providerPath, String key) { + try { + if (providerPath != null) { + DynMethods.builder("set") + .impl(Class.forName(HADOOP_CONFIGURATION_CLASS), String.class, String.class) + .buildChecked() + .invoke(configuration, HADOOP_SECURITY_CREDENTIAL_PATH, providerPath); + + char[] password = + DynMethods.builder("getPassword") + .impl(Class.forName(HADOOP_CONFIGURATION_CLASS), String.class) + .buildChecked() + .invoke(configuration, key); + if (password != null) { + return new String(password); + } + } + } catch (ClassNotFoundException exception) { + throw new RuntimeException(exception); + } catch (NoSuchMethodException exception) { + LOG.warn("Could not retrieve password for " + key, exception); + throw new RuntimeException(exception); + } + return null; + } + + /** + * Method to get the password from the configuration map if available. Otherwise, get it from the + * Hadoop CredentialProvider if Hadoop classes are available + * + * @param confMap configuration map + * @param key param + * @return password + */ + public static String getPassword(Map confMap, String key) { + String password = confMap.get(key); + boolean hadoopCredentialProviderAvailable = false; + Object hadoopConfiguration = null; + if (password == null) { + try { + hadoopConfiguration = + DynConstructors.builder().impl(HADOOP_CONFIGURATION_CLASS).build().newInstance(); + hadoopCredentialProviderAvailable = true; + } catch (Exception exception) { + LOG.warn("Hadoop credential provider is unavailable", exception); + throw new RuntimeException(exception); + } + } + if (password == null && hadoopCredentialProviderAvailable) { + password = + getPasswordFromCredentialProvider( + hadoopConfiguration, confMap.get(JdbcConnectionParams.SSL_STORE_PASSWORD_PATH), key); + } + return password; + } } diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/adapter/SQLResultSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/adapter/SQLResultSet.java index 70c8ff4fe57..32f1c88bc9f 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/adapter/SQLResultSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/adapter/SQLResultSet.java @@ -22,7 +22,6 @@ import java.math.BigDecimal; import java.net.URL; import java.sql.*; -import java.util.Calendar; import java.util.Map; @SuppressWarnings("deprecation") @@ -436,36 +435,6 @@ default Array getArray(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("Method not supported"); } - @Override - default Date getDate(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - - @Override - default Date getDate(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - - @Override - default Time getTime(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - - @Override - default Time getTime(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - - @Override - default Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - - @Override - default Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException("Method not supported"); - } - @Override default URL getURL(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("Method not supported"); diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowColumnarBatchRow.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowColumnarBatchRow.java index 373867069b4..15bcb7bf872 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowColumnarBatchRow.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowColumnarBatchRow.java @@ -21,10 +21,10 @@ import java.sql.Timestamp; import java.time.LocalDateTime; import org.apache.arrow.vector.util.DateUtility; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.common.DateUtils; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalDayTime; import org.apache.kyuubi.jdbc.hive.common.HiveIntervalYearMonth; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; public class ArrowColumnarBatchRow { public int rowId; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowUtils.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowUtils.java index 9a777d4c240..835df35fe70 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowUtils.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/arrow/ArrowUtils.java @@ -30,8 +30,8 @@ import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.types.pojo.FieldType; import org.apache.arrow.vector.types.pojo.Schema; -import org.apache.hive.service.rpc.thrift.TTypeId; import org.apache.kyuubi.jdbc.hive.JdbcColumnAttributes; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; public class ArrowUtils { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthentication.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthentication.java index a7683523f49..a137fbb9946 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthentication.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthentication.java @@ -37,6 +37,7 @@ import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,8 +49,8 @@ public class KerberosAuthentication { private KerberosPrincipal principal = null; private final Configuration configuration; - KerberosAuthentication() { - this.configuration = createLoginFromTgtCacheConfiguration(); + KerberosAuthentication(String ticketCache) { + this.configuration = createLoginFromTgtCacheConfiguration(ticketCache); } KerberosAuthentication(String principal, String keytabLocation) { @@ -96,14 +97,16 @@ private static KerberosPrincipal createKerberosPrincipal(String principal) { } } - private static Configuration createLoginFromTgtCacheConfiguration() { + private static Configuration createLoginFromTgtCacheConfiguration(String ticketCache) { ImmutableMap.Builder optionsBuilder = ImmutableMap.builder() .put("useTicketCache", "true") .put("renewTGT", "true"); - String ticketCache = System.getenv("KRB5CCNAME"); - if (ticketCache != null) { + if (StringUtils.isBlank(ticketCache)) { + ticketCache = System.getenv("KRB5CCNAME"); + } + if (StringUtils.isNotBlank(ticketCache)) { optionsBuilder.put("ticketCache", ticketCache); } return createConfiguration(optionsBuilder); diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthenticationManager.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthenticationManager.java index 92927985fde..3df9aa8366d 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthenticationManager.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosAuthenticationManager.java @@ -27,9 +27,10 @@ public class KerberosAuthenticationManager { private static final Map KEYTAB_AUTHENTICATION_CACHE = new ConcurrentHashMap<>(); - public static synchronized CachingKerberosAuthentication getTgtCacheAuthentication() { + public static synchronized CachingKerberosAuthentication getTgtCacheAuthentication( + String ticketCache) { if (GLOBAL_TGT_CACHE_AUTHENTICATION == null) { - KerberosAuthentication tgtCacheAuth = new KerberosAuthentication(); + KerberosAuthentication tgtCacheAuth = new KerberosAuthentication(ticketCache); GLOBAL_TGT_CACHE_AUTHENTICATION = new CachingKerberosAuthentication(tgtCacheAuth); } return GLOBAL_TGT_CACHE_AUTHENTICATION; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosSaslHelper.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosSaslHelper.java index 67ac6e1663e..e3fb6729365 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosSaslHelper.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/KerberosSaslHelper.java @@ -20,8 +20,8 @@ import java.util.Map; import javax.security.auth.Subject; import javax.security.sasl.SaslException; -import org.apache.thrift.transport.TSaslClientTransport; -import org.apache.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TSaslClientTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/PlainSaslHelper.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/PlainSaslHelper.java index 62b4898e24c..43272e48da5 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/PlainSaslHelper.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/PlainSaslHelper.java @@ -20,8 +20,8 @@ import java.util.HashMap; import javax.security.auth.callback.*; import javax.security.sasl.SaslException; -import org.apache.thrift.transport.TSaslClientTransport; -import org.apache.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TSaslClientTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; public final class PlainSaslHelper { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TFilterTransport.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TFilterTransport.java index 1c7da82fe1e..6d462717b89 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TFilterTransport.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TFilterTransport.java @@ -17,8 +17,8 @@ package org.apache.kyuubi.jdbc.hive.auth; -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransportException; /** * Transport that simply wraps another transport. This is the equivalent of FilterInputStream for diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java index c0785aeed67..be7e581eefe 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java @@ -20,8 +20,8 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import javax.security.auth.Subject; -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransportException; /** * This is used on the client side, where the API explicitly opens transport to the server using the diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/ThriftUtils.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/ThriftUtils.java index 24f2bf53abd..b76401d50ce 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/ThriftUtils.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/ThriftUtils.java @@ -19,10 +19,10 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; -import org.apache.thrift.transport.TSSLTransportFactory; -import org.apache.thrift.transport.TSocket; -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; +import org.apache.kyuubi.shaded.thrift.transport.TSSLTransportFactory; +import org.apache.kyuubi.shaded.thrift.transport.TSocket; +import org.apache.kyuubi.shaded.thrift.transport.TTransport; +import org.apache.kyuubi.shaded.thrift.transport.TTransportException; /** * This class helps in some aspects of authentication. It creates the proper Thrift classes for the diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBasedSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBasedSet.java index 675f4b92d2c..c775f0491ca 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBasedSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBasedSet.java @@ -21,12 +21,12 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import org.apache.hive.service.rpc.thrift.TColumn; -import org.apache.hive.service.rpc.thrift.TRowSet; -import org.apache.thrift.TException; -import org.apache.thrift.protocol.TCompactProtocol; -import org.apache.thrift.protocol.TProtocol; -import org.apache.thrift.transport.TIOStreamTransport; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TColumn; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TRowSet; +import org.apache.kyuubi.shaded.thrift.TException; +import org.apache.kyuubi.shaded.thrift.protocol.TCompactProtocol; +import org.apache.kyuubi.shaded.thrift.protocol.TProtocol; +import org.apache.kyuubi.shaded.thrift.transport.TIOStreamTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBuffer.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBuffer.java index bd5124f9524..0d1bd445d44 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBuffer.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnBuffer.java @@ -23,8 +23,8 @@ import java.util.Arrays; import java.util.BitSet; import java.util.List; -import org.apache.hive.service.rpc.thrift.TColumn; -import org.apache.hive.service.rpc.thrift.TTypeId; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TColumn; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId; /** ColumnBuffer */ public class ColumnBuffer extends AbstractList { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnValue.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnValue.java index 291b791c81d..2efe35618fa 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnValue.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/ColumnValue.java @@ -17,7 +17,7 @@ package org.apache.kyuubi.jdbc.hive.cli; -import org.apache.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; /** Protocols before HIVE_CLI_SERVICE_PROTOCOL_V6 (used by RowBasedSet) */ public class ColumnValue { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowBasedSet.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowBasedSet.java index 3e9c48428c5..8eacedd11c6 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowBasedSet.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowBasedSet.java @@ -20,9 +20,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import org.apache.hive.service.rpc.thrift.TColumnValue; -import org.apache.hive.service.rpc.thrift.TRow; -import org.apache.hive.service.rpc.thrift.TRowSet; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TColumnValue; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TRow; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TRowSet; /** RowBasedSet */ public class RowBasedSet implements RowSet { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowSetFactory.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowSetFactory.java index 48b7e4ad894..0b3afbf35c6 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowSetFactory.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/cli/RowSetFactory.java @@ -17,11 +17,11 @@ package org.apache.kyuubi.jdbc.hive.cli; -import static org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6; +import static org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6; -import org.apache.hive.service.rpc.thrift.TProtocolVersion; -import org.apache.hive.service.rpc.thrift.TRowSet; -import org.apache.thrift.TException; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TRowSet; +import org.apache.kyuubi.shaded.thrift.TException; public class RowSetFactory { diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/logs/InPlaceUpdateStream.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/logs/InPlaceUpdateStream.java index 8ca106e2146..266d88c26b1 100644 --- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/logs/InPlaceUpdateStream.java +++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/logs/InPlaceUpdateStream.java @@ -17,7 +17,7 @@ package org.apache.kyuubi.jdbc.hive.logs; -import org.apache.hive.service.rpc.thrift.TProgressUpdateResp; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProgressUpdateResp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/TestKyuubiPreparedStatement.java b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/TestKyuubiPreparedStatement.java index 522c209e7aa..df0c2d84b78 100644 --- a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/TestKyuubiPreparedStatement.java +++ b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/TestKyuubiPreparedStatement.java @@ -23,8 +23,8 @@ import static org.mockito.Mockito.when; import java.sql.SQLException; -import org.apache.hive.service.rpc.thrift.*; -import org.apache.hive.service.rpc.thrift.TCLIService.Iface; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.*; +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; diff --git a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/UtilsTest.java b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/UtilsTest.java index b01957b3e43..fc4a55d9ff2 100644 --- a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/UtilsTest.java +++ b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/UtilsTest.java @@ -111,6 +111,17 @@ public static Collection data() throws UnsupportedEncodingException { StandardCharsets.UTF_8.toString()) .replaceAll("\\+", "%20") + "#k4=v4" + }, + { + "hostname", + "10018", + "catalog", + "db", + new ImmutableMap.Builder() + .put("k2", "v2") + .put("k3", "hostname:10018") + .build(), + "jdbc:hive2://hostname:10018/catalog/db;k1=v1?k2=v2;k3=hostname:10018" } }); } diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java index 904ecb6c9d6..3e59d0c5b67 100644 --- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java @@ -70,13 +70,12 @@ public String deleteEngine( } public List listEngines( - String engineType, String shareLevel, String subdomain, String hs2ProxyUser, String all) { + String engineType, String shareLevel, String subdomain, String hs2ProxyUser) { Map params = new HashMap<>(); params.put("type", engineType); params.put("sharelevel", shareLevel); params.put("subdomain", subdomain); params.put("hive.server2.proxy.user", hs2ProxyUser); - params.put("all", all); Engine[] result = this.getClient() .get(API_BASE_PATH + "/engine", params, Engine[].class, client.getAuthHeader()); diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/BatchRestApi.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/BatchRestApi.java index 7d113308df1..e6f9577b345 100644 --- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/BatchRestApi.java +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/BatchRestApi.java @@ -23,8 +23,11 @@ import org.apache.kyuubi.client.api.v1.dto.*; import org.apache.kyuubi.client.util.JsonUtils; import org.apache.kyuubi.client.util.VersionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BatchRestApi { + static final Logger LOG = LoggerFactory.getLogger(BatchRestApi.class); private KyuubiRestClient client; @@ -101,7 +104,15 @@ public OperationLog getBatchLocalLog(String batchId, int from, int size) { return this.getClient().get(path, params, OperationLog.class, client.getAuthHeader()); } + /** + * hs2ProxyUser for delete batch is deprecated since 1.8.1, please use {@link + * #deleteBatch(String)} instead. + */ + @Deprecated public CloseBatchResponse deleteBatch(String batchId, String hs2ProxyUser) { + LOG.warn( + "The method `deleteBatch(batchId, hs2ProxyUser)` is deprecated since 1.8.1, " + + "using `deleteBatch(batchId)` instead."); Map params = new HashMap<>(); params.put("hive.server2.proxy.user", hs2ProxyUser); @@ -109,6 +120,11 @@ public CloseBatchResponse deleteBatch(String batchId, String hs2ProxyUser) { return this.getClient().delete(path, params, CloseBatchResponse.class, client.getAuthHeader()); } + public CloseBatchResponse deleteBatch(String batchId) { + String path = String.format("%s/%s", API_BASE_PATH, batchId); + return this.getClient().delete(path, null, CloseBatchResponse.class, client.getAuthHeader()); + } + private IRestClient getClient() { return this.client.getHttpClient(); } diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/KyuubiOperationEvent.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/KyuubiOperationEvent.java index 13c40eecf78..ec583954216 100644 --- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/KyuubiOperationEvent.java +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/KyuubiOperationEvent.java @@ -51,6 +51,8 @@ public class KyuubiOperationEvent { private Map metrics; + private OperationProgress progress; + public KyuubiOperationEvent() {} public KyuubiOperationEvent( @@ -68,7 +70,8 @@ public KyuubiOperationEvent( String sessionUser, String sessionType, String kyuubiInstance, - Map metrics) { + Map metrics, + OperationProgress progress) { this.statementId = statementId; this.remoteId = remoteId; this.statement = statement; @@ -84,6 +87,7 @@ public KyuubiOperationEvent( this.sessionType = sessionType; this.kyuubiInstance = kyuubiInstance; this.metrics = metrics; + this.progress = progress; } public static KyuubiOperationEvent.KyuubiOperationEventBuilder builder() { @@ -121,6 +125,8 @@ public static class KyuubiOperationEventBuilder { private Map metrics; + private OperationProgress progress; + public KyuubiOperationEventBuilder() {} public KyuubiOperationEvent.KyuubiOperationEventBuilder statementId(final String statementId) { @@ -201,6 +207,12 @@ public KyuubiOperationEvent.KyuubiOperationEventBuilder metrics( return this; } + public KyuubiOperationEvent.KyuubiOperationEventBuilder progress( + final OperationProgress progress) { + this.progress = progress; + return this; + } + public KyuubiOperationEvent build() { return new KyuubiOperationEvent( statementId, @@ -217,7 +229,8 @@ public KyuubiOperationEvent build() { sessionUser, sessionType, kyuubiInstance, - metrics); + metrics, + progress); } } @@ -340,4 +353,12 @@ public Map getMetrics() { public void setMetrics(Map metrics) { this.metrics = metrics; } + + public OperationProgress getProgress() { + return progress; + } + + public void setProgress(OperationProgress progress) { + this.progress = progress; + } } diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationData.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationData.java index 70c2dd3f3a1..8e5bafc6e28 100644 --- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationData.java +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationData.java @@ -25,6 +25,7 @@ public class OperationData { private String identifier; + private String remoteId; private String statement; private String state; private Long createTime; @@ -36,11 +37,13 @@ public class OperationData { private String sessionType; private String kyuubiInstance; private Map metrics; + private OperationProgress progress; public OperationData() {} public OperationData( String identifier, + String remoteId, String statement, String state, Long createTime, @@ -51,8 +54,10 @@ public OperationData( String sessionUser, String sessionType, String kyuubiInstance, - Map metrics) { + Map metrics, + OperationProgress progress) { this.identifier = identifier; + this.remoteId = remoteId; this.statement = statement; this.state = state; this.createTime = createTime; @@ -64,6 +69,7 @@ public OperationData( this.sessionType = sessionType; this.kyuubiInstance = kyuubiInstance; this.metrics = metrics; + this.progress = progress; } public String getIdentifier() { @@ -74,6 +80,14 @@ public void setIdentifier(String identifier) { this.identifier = identifier; } + public String getRemoteId() { + return remoteId; + } + + public void setRemoteId(String remoteId) { + this.remoteId = remoteId; + } + public String getStatement() { return statement; } @@ -165,6 +179,14 @@ public void setMetrics(Map metrics) { this.metrics = metrics; } + public OperationProgress getProgress() { + return progress; + } + + public void setProgress(OperationProgress progress) { + this.progress = progress; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationProgress.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationProgress.java new file mode 100644 index 00000000000..8668f2f30f7 --- /dev/null +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/OperationProgress.java @@ -0,0 +1,133 @@ +/* + * 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.kyuubi.client.api.v1.dto; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class OperationProgress { + private List headerNames; + private List> rows; + private double progressedPercentage; + private String status; + private String footerSummary; + private long startTime; + + public OperationProgress() {} + + public OperationProgress( + List headerNames, + List> rows, + double progressedPercentage, + String status, + String footerSummary, + long startTime) { + this.headerNames = headerNames; + this.rows = rows; + this.progressedPercentage = progressedPercentage; + this.status = status; + this.footerSummary = footerSummary; + this.startTime = startTime; + } + + public List getHeaderNames() { + if (headerNames == null) { + return Collections.emptyList(); + } + return headerNames; + } + + public void setHeaderNames(List headerNames) { + this.headerNames = headerNames; + } + + public List> getRows() { + if (rows == null) { + return Collections.emptyList(); + } + return rows; + } + + public void setRows(List> rows) { + this.rows = rows; + } + + public double getProgressedPercentage() { + return progressedPercentage; + } + + public void setProgressedPercentage(double progressedPercentage) { + this.progressedPercentage = progressedPercentage; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getFooterSummary() { + return footerSummary; + } + + public void setFooterSummary(String footerSummary) { + this.footerSummary = footerSummary; + } + + public long getStartTime() { + return startTime; + } + + public void setStartTime(long startTime) { + this.startTime = startTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OperationProgress that = (OperationProgress) o; + return Double.compare(getProgressedPercentage(), that.getProgressedPercentage()) == 0 + && getStartTime() == that.getStartTime() + && Objects.equals(getHeaderNames(), that.getHeaderNames()) + && Objects.equals(getRows(), that.getRows()) + && Objects.equals(getStatus(), that.getStatus()) + && Objects.equals(getFooterSummary(), that.getFooterSummary()); + } + + @Override + public int hashCode() { + return Objects.hash( + getHeaderNames(), + getRows(), + getProgressedPercentage(), + getStatus(), + getFooterSummary(), + getStartTime()); + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/SessionData.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/SessionData.java index ae7dfdec984..30a4eb51540 100644 --- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/SessionData.java +++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/api/v1/dto/SessionData.java @@ -25,6 +25,7 @@ public class SessionData { private String identifier; + private String remoteId; private String user; private String ipAddr; private Map conf; @@ -40,6 +41,7 @@ public SessionData() {} public SessionData( String identifier, + String remoteId, String user, String ipAddr, Map conf, @@ -51,6 +53,7 @@ public SessionData( String kyuubiInstance, String engineId) { this.identifier = identifier; + this.remoteId = remoteId; this.user = user; this.ipAddr = ipAddr; this.conf = conf; @@ -71,6 +74,14 @@ public void setIdentifier(String identifier) { this.identifier = identifier; } + public String getRemoteId() { + return remoteId; + } + + public void setRemoteId(String remoteId) { + this.remoteId = remoteId; + } + public String getUser() { return user; } diff --git a/kyuubi-rest-client/src/test/java/org/apache/kyuubi/client/BatchRestClientTest.java b/kyuubi-rest-client/src/test/java/org/apache/kyuubi/client/BatchRestClientTest.java index 80fb1c4b95f..9715460ca5f 100644 --- a/kyuubi-rest-client/src/test/java/org/apache/kyuubi/client/BatchRestClientTest.java +++ b/kyuubi-rest-client/src/test/java/org/apache/kyuubi/client/BatchRestClientTest.java @@ -267,13 +267,13 @@ public void getOperationLogTest() { public void deleteBatchTest() { // test spnego auth BatchTestServlet.setAuthSchema(NEGOTIATE_AUTH); - CloseBatchResponse response = spnegoBatchRestApi.deleteBatch("71535", "b_test"); + CloseBatchResponse response = spnegoBatchRestApi.deleteBatch("71535"); assertTrue(response.isSuccess()); // test basic auth BatchTestServlet.setAuthSchema(BASIC_AUTH); BatchTestServlet.allowAnonymous(false); - response = basicBatchRestApi.deleteBatch("71535", "b_test"); + response = basicBatchRestApi.deleteBatch("71535"); assertTrue(response.isSuccess()); } } diff --git a/kyuubi-server/pom.xml b/kyuubi-server/pom.xml index 56155a27bec..17fd851d2ec 100644 --- a/kyuubi-server/pom.xml +++ b/kyuubi-server/pom.xml @@ -187,6 +187,21 @@ + + org.apache.thrift + libfb303 + + + + org.apache.thrift + libthrift + + + + org.apache.hive + hive-service-rpc + + commons-lang commons-lang diff --git a/kyuubi-server/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier b/kyuubi-server/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier index 3b1f088f90a..65e2965c025 100644 --- a/kyuubi-server/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier +++ b/kyuubi-server/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.credentials.HadoopDelegationTokenProvider b/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.credentials.HadoopDelegationTokenProvider index 1d931c8c79d..95d6e1987fa 100644 --- a/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.credentials.HadoopDelegationTokenProvider +++ b/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.credentials.HadoopDelegationTokenProvider @@ -6,7 +6,7 @@ # (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 +# 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, diff --git a/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.engine.ApplicationOperation b/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.engine.ApplicationOperation index 712bd8f2e2f..b6df64bd9ba 100644 --- a/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.engine.ApplicationOperation +++ b/kyuubi-server/src/main/resources/META-INF/services/org.apache.kyuubi.engine.ApplicationOperation @@ -6,7 +6,7 @@ # (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 +# 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, @@ -15,6 +15,6 @@ # limitations under the License. # -org.apache.kyuubi.engine.YarnApplicationOperation org.apache.kyuubi.engine.JpsApplicationOperation org.apache.kyuubi.engine.KubernetesApplicationOperation +org.apache.kyuubi.engine.YarnApplicationOperation diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/client/KyuubiSyncThriftClient.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/client/KyuubiSyncThriftClient.scala index ad7191c090c..0dc6692da43 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/client/KyuubiSyncThriftClient.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/client/KyuubiSyncThriftClient.scala @@ -26,9 +26,6 @@ import scala.concurrent.ExecutionException import scala.concurrent.duration.Duration import com.google.common.annotations.VisibleForTesting -import org.apache.hive.service.rpc.thrift._ -import org.apache.thrift.protocol.{TBinaryProtocol, TProtocol} -import org.apache.thrift.transport.TSocket import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils} import org.apache.kyuubi.config.KyuubiConf @@ -38,7 +35,11 @@ import org.apache.kyuubi.operation.FetchOrientation import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.service.authentication.PlainSASLHelper import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.protocol.{TBinaryProtocol, TProtocol} +import org.apache.kyuubi.shaded.thrift.transport.TSocket import org.apache.kyuubi.util.{ThreadUtils, ThriftUtils} +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay class KyuubiSyncThriftClient private ( protocol: TProtocol, @@ -98,10 +99,11 @@ class KyuubiSyncThriftClient private ( remoteEngineBroken = false } catch { case e: Throwable => - warn(s"The engine[$engineId] alive probe fails", e) + val engineIdStr = engineId.getOrElse("") + warn(s"The engine[$engineIdStr] alive probe fails", e) val now = System.currentTimeMillis() if (now - engineLastAlive > engineAliveTimeout) { - error(s"Mark the engine[$engineId] not alive with no recent alive probe" + + error(s"Mark the engine[$engineIdStr] not alive with no recent alive probe" + s" success: ${now - engineLastAlive} ms exceeds timeout $engineAliveTimeout ms") remoteEngineBroken = true } @@ -125,7 +127,8 @@ class KyuubiSyncThriftClient private ( } } engineLastAlive = System.currentTimeMillis() - engineAliveThreadPool.scheduleWithFixedDelay( + scheduleTolerableRunnableWithFixedDelay( + engineAliveThreadPool, task, engineAliveProbeInterval, engineAliveProbeInterval, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/credentials/HadoopCredentialsManager.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/credentials/HadoopCredentialsManager.scala index b51255b716f..92b201718b4 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/credentials/HadoopCredentialsManager.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/credentials/HadoopCredentialsManager.scala @@ -33,6 +33,7 @@ import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.service.AbstractService import org.apache.kyuubi.util.{KyuubiHadoopUtils, ThreadUtils} +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay import org.apache.kyuubi.util.reflect.ReflectUtils._ /** @@ -107,7 +108,7 @@ class HadoopCredentialsManager private (name: String) extends AbstractService(na s" Check your configuration to see if security is disabled or not." + s" If security is enabled, some configurations of ${provider.serviceName} " + s" might be missing, please check the configurations in " + - s" https://kyuubi.readthedocs.io/en/latest/security" + + s" https://kyuubi.readthedocs.io/en/master/security" + s"/hadoop_credentials_manager.html#required-security-configs") provider.close() } @@ -299,7 +300,8 @@ class HadoopCredentialsManager private (name: String) extends AbstractService(na } credentialsTimeoutChecker.foreach { executor => - executor.scheduleWithFixedDelay( + scheduleTolerableRunnableWithFixedDelay( + executor, checkTask, credentialsCheckInterval, credentialsCheckInterval, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala index 6122a6f138f..2bd8554036e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala @@ -307,6 +307,19 @@ private[kyuubi] class EngineRef( } } + /** + * Deregister the engine from engine space with the given host and port on connection failure. + * + * @param discoveryClient the zookeeper client to get or create engine instance + * @param hostPort the existing engine host and port + */ + def deregister(discoveryClient: DiscoveryClient, hostPort: (String, Int)): Unit = + tryWithLock(discoveryClient) { + if (discoveryClient.getServerHost(engineSpace) == Option(hostPort)) { + discoveryClient.delete(engineSpace) + } + } + def close(): Unit = { if (shareLevel == CONNECTION && builder != null) { try { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationAuditLogger.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationAuditLogger.scala index 731b9d7b5ba..565c8a694e5 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationAuditLogger.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationAuditLogger.scala @@ -17,25 +17,40 @@ package org.apache.kyuubi.engine +import scala.collection.JavaConverters._ + import io.fabric8.kubernetes.api.model.Pod import org.apache.kyuubi.Logging -import org.apache.kyuubi.engine.KubernetesApplicationOperation.{toApplicationState, LABEL_KYUUBI_UNIQUE_KEY, SPARK_APP_ID_LABEL} +import org.apache.kyuubi.config.KyuubiConf.KubernetesApplicationStateSource.KubernetesApplicationStateSource +import org.apache.kyuubi.engine.KubernetesApplicationOperation.{toApplicationStateAndError, LABEL_KYUUBI_UNIQUE_KEY, SPARK_APP_ID_LABEL} object KubernetesApplicationAuditLogger extends Logging { final private val AUDIT_BUFFER = new ThreadLocal[StringBuilder]() { override protected def initialValue: StringBuilder = new StringBuilder() } - def audit(kubernetesInfo: KubernetesInfo, pod: Pod): Unit = { + def audit( + kubernetesInfo: KubernetesInfo, + pod: Pod, + appStateSource: KubernetesApplicationStateSource, + appStateContainer: String): Unit = { val sb = AUDIT_BUFFER.get() sb.setLength(0) sb.append(s"label=${pod.getMetadata.getLabels.get(LABEL_KYUUBI_UNIQUE_KEY)}").append("\t") sb.append(s"context=${kubernetesInfo.context.orNull}").append("\t") sb.append(s"namespace=${kubernetesInfo.namespace.orNull}").append("\t") sb.append(s"pod=${pod.getMetadata.getName}").append("\t") + sb.append(s"podState=${pod.getStatus.getPhase}").append("\t") + val containerStatuses = pod.getStatus.getContainerStatuses.asScala.map { containerState => + s"${containerState.getName}->${containerState.getState}" + }.mkString("[", ",", "]") + sb.append(s"containers=$containerStatuses").append("\t") sb.append(s"appId=${pod.getMetadata.getLabels.get(SPARK_APP_ID_LABEL)}").append("\t") - sb.append(s"appState=${toApplicationState(pod.getStatus.getPhase)}") + val (appState, appError) = + toApplicationStateAndError(pod, appStateSource, appStateContainer) + sb.append(s"appState=$appState").append("\t") + sb.append(s"appError='${appError.getOrElse("")}'") info(sb.toString()) } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala index 16a0c29d149..6afe3257be9 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala @@ -18,22 +18,26 @@ package org.apache.kyuubi.engine import java.util.Locale -import java.util.concurrent.{ConcurrentHashMap, TimeUnit} +import java.util.concurrent.{ConcurrentHashMap, ScheduledExecutorService, TimeUnit} import scala.collection.JavaConverters._ +import scala.util.control.NonFatal import com.google.common.cache.{Cache, CacheBuilder, RemovalNotification} -import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.{ContainerState, Pod} import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.informers.{ResourceEventHandler, SharedIndexInformer} import org.apache.kyuubi.{KyuubiException, Logging, Utils} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.{KubernetesApplicationStateSource, KubernetesCleanupDriverPodStrategy} +import org.apache.kyuubi.config.KyuubiConf.KubernetesApplicationStateSource.KubernetesApplicationStateSource +import org.apache.kyuubi.config.KyuubiConf.KubernetesCleanupDriverPodStrategy.{ALL, COMPLETED, NONE} import org.apache.kyuubi.engine.ApplicationState.{isTerminated, ApplicationState, FAILED, FINISHED, NOT_FOUND, PENDING, RUNNING, UNKNOWN} -import org.apache.kyuubi.engine.KubernetesApplicationOperation.{toApplicationState, toLabel, LABEL_KYUUBI_UNIQUE_KEY, SPARK_APP_ID_LABEL} -import org.apache.kyuubi.util.KubernetesUtils +import org.apache.kyuubi.util.{KubernetesUtils, ThreadUtils} class KubernetesApplicationOperation extends ApplicationOperation with Logging { + import KubernetesApplicationOperation._ private val kubernetesClients: ConcurrentHashMap[KubernetesInfo, KubernetesClient] = new ConcurrentHashMap[KubernetesInfo, KubernetesClient] @@ -48,12 +52,20 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { private def allowedNamespaces: Set[String] = kyuubiConf.get(KyuubiConf.KUBERNETES_NAMESPACE_ALLOW_LIST) + private def appStateSource: KubernetesApplicationStateSource = + KubernetesApplicationStateSource.withName( + kyuubiConf.get(KyuubiConf.KUBERNETES_APPLICATION_STATE_SOURCE)) + private def appStateContainer: String = + kyuubiConf.get(KyuubiConf.KUBERNETES_APPLICATION_STATE_CONTAINER) + // key is kyuubi_unique_key - private val appInfoStore: ConcurrentHashMap[String, ApplicationInfo] = - new ConcurrentHashMap[String, ApplicationInfo] + private val appInfoStore: ConcurrentHashMap[String, (KubernetesInfo, ApplicationInfo)] = + new ConcurrentHashMap[String, (KubernetesInfo, ApplicationInfo)] // key is kyuubi_unique_key private var cleanupTerminatedAppInfoTrigger: Cache[String, ApplicationState] = _ + private var expireCleanUpTriggerCacheExecutor: ScheduledExecutorService = _ + private def getOrCreateKubernetesClient(kubernetesInfo: KubernetesInfo): KubernetesClient = { checkKubernetesInfo(kubernetesInfo) kubernetesClients.computeIfAbsent(kubernetesInfo, kInfo => buildKubernetesClient(kInfo)) @@ -98,15 +110,65 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { submitTimeout = conf.get(KyuubiConf.ENGINE_KUBERNETES_SUBMIT_TIMEOUT) // Defer cleaning terminated application information val retainPeriod = conf.get(KyuubiConf.KUBERNETES_TERMINATED_APPLICATION_RETAIN_PERIOD) + val cleanupDriverPodStrategy = KubernetesCleanupDriverPodStrategy.withName( + conf.get(KyuubiConf.KUBERNETES_SPARK_CLEANUP_TERMINATED_DRIVER_POD_KIND)) + val cleanupDriverPodCheckInterval = conf.get( + KyuubiConf.KUBERNETES_SPARK_CLEANUP_TERMINATED_DRIVER_POD_KIND_CHECK_INTERVAL) cleanupTerminatedAppInfoTrigger = CacheBuilder.newBuilder() .expireAfterWrite(retainPeriod, TimeUnit.MILLISECONDS) .removalListener((notification: RemovalNotification[String, ApplicationState]) => { - Option(appInfoStore.remove(notification.getKey)).foreach { removed => - info(s"Remove terminated application ${removed.id} with " + - s"[${toLabel(notification.getKey)}, state: ${removed.state}]") + Option(appInfoStore.remove(notification.getKey)).foreach { case (kubernetesInfo, removed) => + val appLabel = notification.getKey + val shouldDelete = cleanupDriverPodStrategy match { + case NONE => false + case ALL => true + case COMPLETED => !ApplicationState.isFailed(notification.getValue) + } + if (shouldDelete) { + val podName = removed.name + try { + val kubernetesClient = getOrCreateKubernetesClient(kubernetesInfo) + val deleted = if (podName == null) { + !kubernetesClient.pods() + .withLabel(LABEL_KYUUBI_UNIQUE_KEY, appLabel) + .delete().isEmpty + } else { + !kubernetesClient.pods().withName(podName).delete().isEmpty + } + if (deleted) { + info(s"[$kubernetesInfo] Operation of delete pod $podName with" + + s" ${toLabel(appLabel)} is completed.") + } else { + warn(s"[$kubernetesInfo] Failed to delete pod $podName with ${toLabel(appLabel)}.") + } + } catch { + case NonFatal(e) => error( + s"[$kubernetesInfo] Failed to delete pod $podName with ${toLabel(appLabel)}", + e) + } + } + info(s"Remove terminated application $removed with ${toLabel(appLabel)}") } }) .build() + expireCleanUpTriggerCacheExecutor = ThreadUtils.newDaemonSingleThreadScheduledExecutor( + "pod-cleanup-trigger-thread") + ThreadUtils.scheduleTolerableRunnableWithFixedDelay( + expireCleanUpTriggerCacheExecutor, + () => { + try { + cleanupTerminatedAppInfoTrigger.asMap().asScala.foreach { + case (key, _) => + // do get to trigger cache eviction + cleanupTerminatedAppInfoTrigger.getIfPresent(key) + } + } catch { + case NonFatal(e) => error("Failed to evict clean up terminated app cache", e) + } + }, + cleanupDriverPodCheckInterval, + cleanupDriverPodCheckInterval, + TimeUnit.MILLISECONDS) } override def isSupported(appMgrInfo: ApplicationManagerInfo): Boolean = { @@ -127,7 +189,7 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { debug(s"[$kubernetesInfo] Deleting application[${toLabel(tag)}]'s info from Kubernetes cluster") try { Option(appInfoStore.get(tag)) match { - case Some(info) => + case Some((_, info)) => debug(s"Application[${toLabel(tag)}] is in ${info.state} state") info.state match { case NOT_FOUND | FAILED | UNKNOWN => @@ -167,7 +229,8 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { try { // need to initialize the kubernetes client if not exists getOrCreateKubernetesClient(appMgrInfo.kubernetesInfo) - val appInfo = appInfoStore.getOrDefault(tag, ApplicationInfo.NOT_FOUND) + val (_, appInfo) = + appInfoStore.getOrDefault(tag, appMgrInfo.kubernetesInfo -> ApplicationInfo.NOT_FOUND) (appInfo.state, submitTime) match { // Kyuubi should wait second if pod is not be created case (NOT_FOUND, Some(_submitTime)) => @@ -200,15 +263,15 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { } enginePodInformers.clear() - kubernetesClients.asScala.foreach { case (_, client) => - Utils.tryLogNonFatalError(client.close()) - } - kubernetesClients.clear() - if (cleanupTerminatedAppInfoTrigger != null) { cleanupTerminatedAppInfoTrigger.cleanUp() cleanupTerminatedAppInfoTrigger = null } + + kubernetesClients.asScala.foreach { case (_, client) => + Utils.tryLogNonFatalError(client.close()) + } + kubernetesClients.clear() } private class SparkEnginePodEventHandler(kubernetesInfo: KubernetesInfo) @@ -216,27 +279,39 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { override def onAdd(pod: Pod): Unit = { if (isSparkEnginePod(pod)) { - updateApplicationState(pod) - KubernetesApplicationAuditLogger.audit(kubernetesInfo, pod) + updateApplicationState(kubernetesInfo, pod) + KubernetesApplicationAuditLogger.audit( + kubernetesInfo, + pod, + appStateSource, + appStateContainer) } } override def onUpdate(oldPod: Pod, newPod: Pod): Unit = { if (isSparkEnginePod(newPod)) { - updateApplicationState(newPod) - val appState = toApplicationState(newPod.getStatus.getPhase) + updateApplicationState(kubernetesInfo, newPod) + val appState = toApplicationState(newPod, appStateSource, appStateContainer) if (isTerminated(appState)) { markApplicationTerminated(newPod) } - KubernetesApplicationAuditLogger.audit(kubernetesInfo, newPod) + KubernetesApplicationAuditLogger.audit( + kubernetesInfo, + newPod, + appStateSource, + appStateContainer) } } override def onDelete(pod: Pod, deletedFinalStateUnknown: Boolean): Unit = { if (isSparkEnginePod(pod)) { - updateApplicationState(pod) + updateApplicationState(kubernetesInfo, pod) markApplicationTerminated(pod) - KubernetesApplicationAuditLogger.audit(kubernetesInfo, pod) + KubernetesApplicationAuditLogger.audit( + kubernetesInfo, + pod, + appStateSource, + appStateContainer) } } } @@ -246,22 +321,25 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging { labels.containsKey(LABEL_KYUUBI_UNIQUE_KEY) && labels.containsKey(SPARK_APP_ID_LABEL) } - private def updateApplicationState(pod: Pod): Unit = { - val appState = toApplicationState(pod.getStatus.getPhase) + private def updateApplicationState(kubernetesInfo: KubernetesInfo, pod: Pod): Unit = { + val (appState, appError) = + toApplicationStateAndError(pod, appStateSource, appStateContainer) debug(s"Driver Informer changes pod: ${pod.getMetadata.getName} to state: $appState") appInfoStore.put( pod.getMetadata.getLabels.get(LABEL_KYUUBI_UNIQUE_KEY), - ApplicationInfo( + kubernetesInfo -> ApplicationInfo( id = pod.getMetadata.getLabels.get(SPARK_APP_ID_LABEL), name = pod.getMetadata.getName, state = appState, - error = Option(pod.getStatus.getReason))) + error = appError)) } private def markApplicationTerminated(pod: Pod): Unit = synchronized { val key = pod.getMetadata.getLabels.get(LABEL_KYUUBI_UNIQUE_KEY) if (cleanupTerminatedAppInfoTrigger.getIfPresent(key) == null) { - cleanupTerminatedAppInfoTrigger.put(key, toApplicationState(pod.getStatus.getPhase)) + cleanupTerminatedAppInfoTrigger.put( + key, + toApplicationState(pod, appStateSource, appStateContainer)) } } } @@ -274,16 +352,62 @@ object KubernetesApplicationOperation extends Logging { def toLabel(tag: String): String = s"label: $LABEL_KYUUBI_UNIQUE_KEY=$tag" - def toApplicationState(state: String): ApplicationState = state match { - // https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/core/types.go#L2396 - // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/ + def toApplicationState( + pod: Pod, + appStateSource: KubernetesApplicationStateSource, + appStateContainer: String): ApplicationState = { + toApplicationStateAndError(pod, appStateSource, appStateContainer)._1 + } + + def toApplicationStateAndError( + pod: Pod, + appStateSource: KubernetesApplicationStateSource, + appStateContainer: String): (ApplicationState, Option[String]) = { + val podName = pod.getMetadata.getName + val containerStateToBuildAppState = appStateSource match { + case KubernetesApplicationStateSource.CONTAINER => + pod.getStatus.getContainerStatuses.asScala + .find(cs => appStateContainer.equalsIgnoreCase(cs.getName)).map(_.getState) + case KubernetesApplicationStateSource.POD => None + } + val applicationState = containerStateToBuildAppState.map(containerStateToApplicationState) + .getOrElse(podStateToApplicationState(pod.getStatus.getPhase)) + val applicationError = containerStateToBuildAppState + .map(cs => containerStateToApplicationError(cs).map(r => s"$podName/$appStateContainer[$r]")) + .getOrElse(Option(pod.getStatus.getReason).map(r => s"$podName[$r]")) + applicationState -> applicationError + } + + def containerStateToApplicationState(containerState: ContainerState): ApplicationState = { + // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-states + if (containerState.getWaiting != null) { + PENDING + } else if (containerState.getRunning != null) { + RUNNING + } else if (containerState.getTerminated == null) { + UNKNOWN + } else if (containerState.getTerminated.getExitCode == 0) { + FINISHED + } else { + FAILED + } + } + + def containerStateToApplicationError(containerState: ContainerState): Option[String] = { + // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-states + Option(containerState.getWaiting).map(_.getReason) + .orElse(Option(containerState.getTerminated).map(_.getReason)) + } + + def podStateToApplicationState(podState: String): ApplicationState = podState match { + // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase case "Pending" => PENDING case "Running" => RUNNING case "Succeeded" => FINISHED case "Failed" | "Error" => FAILED case "Unknown" => UNKNOWN case _ => - warn(s"The kubernetes driver pod state: $state is not supported, " + + warn(s"The spark driver pod state: $podState is not supported, " + "mark the application state as UNKNOWN.") UNKNOWN } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala index 84807a62d87..23196bf1ded 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/ProcBuilder.scala @@ -99,7 +99,7 @@ trait ProcBuilder { protected def proxyUser: String - protected val commands: Array[String] + protected val commands: Iterable[String] def conf: KyuubiConf @@ -142,7 +142,7 @@ trait ProcBuilder { } final lazy val processBuilder: ProcessBuilder = { - val pb = new ProcessBuilder(commands: _*) + val pb = new ProcessBuilder(commands.toStream.asJava) val envs = pb.environment() envs.putAll(env.asJava) @@ -287,10 +287,10 @@ trait ProcBuilder { override def toString: String = { if (commands == null) { - super.toString() + super.toString } else { Utils.redactCommandLineArgs(conf, commands).map { - case arg if arg.startsWith("--") => s"\\\n\t$arg" + case arg if arg.startsWith("-") => s"\\\n\t$arg" case arg => arg }.mkString(" ") } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/chat/ChatProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/chat/ChatProcessBuilder.scala index 3e4a20de373..ddf88e14924 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/chat/ChatProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/chat/ChatProcessBuilder.scala @@ -19,20 +19,18 @@ package org.apache.kyuubi.engine.chat import java.io.File import java.nio.file.{Files, Paths} -import java.util -import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import com.google.common.annotations.VisibleForTesting import org.apache.kyuubi.{Logging, SCALA_COMPILE_VERSION, Utils} -import org.apache.kyuubi.Utils.REDACTION_REPLACEMENT_TEXT import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY import org.apache.kyuubi.engine.ProcBuilder import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ class ChatProcessBuilder( override val proxyUser: String, @@ -59,8 +57,8 @@ class ChatProcessBuilder( */ override protected def mainClass: String = "org.apache.kyuubi.engine.chat.ChatEngine" - override protected val commands: Array[String] = { - val buffer = new ArrayBuffer[String]() + override protected val commands: Iterable[String] = { + val buffer = new mutable.ListBuffer[String]() buffer += executable val memory = conf.get(ENGINE_CHAT_MEMORY) @@ -69,8 +67,7 @@ class ChatProcessBuilder( val javaOptions = conf.get(ENGINE_CHAT_JAVA_OPTIONS) javaOptions.foreach(buffer += _) - buffer += "-cp" - val classpathEntries = new util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] mainResource.foreach(classpathEntries.add) mainResource.foreach { path => val parent = Paths.get(path).getParent @@ -88,27 +85,25 @@ class ChatProcessBuilder( val extraCp = conf.get(ENGINE_CHAT_EXTRA_CLASSPATH) extraCp.foreach(classpathEntries.add) - buffer += classpathEntries.asScala.mkString(File.pathSeparator) + buffer ++= genClasspathOption(classpathEntries) + buffer += mainClass - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) - conf.getAll.foreach { case (k, v) => - buffer += "--conf" - buffer += s"$k=$v" - } - buffer.toArray + buffer ++= confKeyValues(conf.getAll) + + buffer } override def toString: String = { if (commands == null) { - super.toString() + super.toString } else { - Utils.redactCommandLineArgs(conf, commands).map { + redactConfValues( + Utils.redactCommandLineArgs(conf, commands), + Set(ENGINE_CHAT_GPT_API_KEY.key)).map { case arg if arg.startsWith("-") || arg == mainClass => s"\\\n\t$arg" - case arg if arg.contains(ENGINE_CHAT_GPT_API_KEY.key) => - s"${ENGINE_CHAT_GPT_API_KEY.key}=$REDACTION_REPLACEMENT_TEXT" case arg => arg }.mkString(" ") } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala index f43adfbc216..a1e8cdcd38b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala @@ -20,8 +20,7 @@ package org.apache.kyuubi.engine.flink import java.io.{File, FilenameFilter} import java.nio.file.{Files, Paths} -import scala.collection.JavaConverters._ -import scala.collection.mutable.{ArrayBuffer, ListBuffer} +import scala.collection.mutable import com.google.common.annotations.VisibleForTesting @@ -32,6 +31,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY import org.apache.kyuubi.engine.{ApplicationManagerInfo, KyuubiApplicationManager, ProcBuilder} import org.apache.kyuubi.engine.flink.FlinkProcessBuilder._ import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ /** * A builder to build flink sql engine progress. @@ -77,18 +77,18 @@ class FlinkProcessBuilder( ApplicationManagerInfo(clusterManager()) } - override protected val commands: Array[String] = { + override protected val commands: Iterable[String] = { KyuubiApplicationManager.tagApplication(engineRefId, shortName, clusterManager(), conf) // unset engine credentials because Flink doesn't support them at the moment conf.unset(KyuubiReservedKeys.KYUUBI_ENGINE_CREDENTIALS_KEY) // flink.execution.target are required in Kyuubi conf currently executionTarget match { case Some("yarn-application") => - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += flinkExecutable buffer += "run-application" - val flinkExtraJars = new ListBuffer[String] + val flinkExtraJars = new mutable.ListBuffer[String] // locate flink sql jars val flinkSqlJars = Paths.get(flinkHome) .resolve("opt") @@ -134,19 +134,14 @@ class FlinkProcessBuilder( buffer += s"$mainClass" buffer += s"${mainResource.get}" - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" - conf.getAll.foreach { case (k, v) => - if (k.startsWith("kyuubi.")) { - buffer += "--conf" - buffer += s"$k=$v" - } - } + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) + + buffer ++= confKeyValues(conf.getAll.filter(_._1.startsWith("kyuubi."))) - buffer.toArray + buffer case _ => - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += executable val memory = conf.get(ENGINE_FLINK_MEMORY) @@ -156,8 +151,7 @@ class FlinkProcessBuilder( buffer += javaOptions.get } - buffer += "-cp" - val classpathEntries = new java.util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] // flink engine runtime jar mainResource.foreach(classpathEntries.add) // flink sql jars @@ -201,17 +195,15 @@ class FlinkProcessBuilder( classpathEntries.add(s"$devHadoopJars${File.separator}*") } } - buffer += classpathEntries.asScala.mkString(File.pathSeparator) + buffer ++= genClasspathOption(classpathEntries) + buffer += mainClass - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) - conf.getAll.foreach { case (k, v) => - buffer += "--conf" - buffer += s"$k=$v" - } - buffer.toArray + buffer ++= confKeyValues(conf.getAll) + + buffer } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala index 61fe55887ea..d8e4454b610 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala @@ -19,10 +19,8 @@ package org.apache.kyuubi.engine.hive import java.io.File import java.nio.file.{Files, Paths} -import java.util -import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import com.google.common.annotations.VisibleForTesting @@ -33,6 +31,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_ENGINE_ID, KYUUBI_SES import org.apache.kyuubi.engine.{KyuubiApplicationManager, ProcBuilder} import org.apache.kyuubi.engine.hive.HiveProcessBuilder._ import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ class HiveProcessBuilder( override val proxyUser: String, @@ -52,9 +51,9 @@ class HiveProcessBuilder( override protected def mainClass: String = "org.apache.kyuubi.engine.hive.HiveSQLEngine" - override protected val commands: Array[String] = { + override protected val commands: Iterable[String] = { KyuubiApplicationManager.tagApplication(engineRefId, shortName, clusterManager(), conf) - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += executable val memory = conf.get(ENGINE_HIVE_MEMORY) @@ -65,8 +64,7 @@ class HiveProcessBuilder( } // -Xmx5g // java options - buffer += "-cp" - val classpathEntries = new util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] // hive engine runtime jar mainResource.foreach(classpathEntries.add) // classpath contains hive configurations, default to hive.home/conf @@ -101,22 +99,16 @@ class HiveProcessBuilder( classpathEntries.add(s"$devHadoopJars${File.separator}*") } } - buffer += classpathEntries.asScala.mkString(File.pathSeparator) + buffer ++= genClasspathOption(classpathEntries) buffer += mainClass - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" - buffer += "--conf" - buffer += s"$KYUUBI_ENGINE_ID=$engineRefId" + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) + buffer ++= confKeyValue(KYUUBI_ENGINE_ID, engineRefId) - for ((k, v) <- conf.getAll) { - buffer += "--conf" - buffer += s"$k=$v" - } - buffer.toArray - } + buffer ++= confKeyValues(conf.getAll) - override def toString: String = Utils.redactCommandLineArgs(conf, commands).mkString("\n") + buffer + } override def shortName: String = "hive" } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala index 14ad53b20a8..2d08d510199 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala @@ -19,20 +19,18 @@ package org.apache.kyuubi.engine.jdbc import java.io.File import java.nio.file.Paths -import java.util -import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import com.google.common.annotations.VisibleForTesting import org.apache.kyuubi.{Logging, SCALA_COMPILE_VERSION, Utils} -import org.apache.kyuubi.Utils.REDACTION_REPLACEMENT_TEXT import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.{ENGINE_JDBC_CONNECTION_PASSWORD, ENGINE_JDBC_CONNECTION_URL, ENGINE_JDBC_EXTRA_CLASSPATH, ENGINE_JDBC_JAVA_OPTIONS, ENGINE_JDBC_MEMORY} import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY import org.apache.kyuubi.engine.ProcBuilder import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ class JdbcProcessBuilder( override val proxyUser: String, @@ -59,11 +57,11 @@ class JdbcProcessBuilder( */ override protected def mainClass: String = "org.apache.kyuubi.engine.jdbc.JdbcSQLEngine" - override protected val commands: Array[String] = { + override protected val commands: Iterable[String] = { require( conf.get(ENGINE_JDBC_CONNECTION_URL).nonEmpty, s"Jdbc server url can not be null! Please set ${ENGINE_JDBC_CONNECTION_URL.key}") - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += executable val memory = conf.get(ENGINE_JDBC_MEMORY) @@ -72,8 +70,7 @@ class JdbcProcessBuilder( val javaOptions = conf.get(ENGINE_JDBC_JAVA_OPTIONS) javaOptions.foreach(buffer += _) - buffer += "-cp" - val classpathEntries = new util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] mainResource.foreach(classpathEntries.add) mainResource.foreach { path => val parent = Paths.get(path).getParent @@ -91,28 +88,27 @@ class JdbcProcessBuilder( val extraCp = conf.get(ENGINE_JDBC_EXTRA_CLASSPATH) extraCp.foreach(classpathEntries.add) - buffer += classpathEntries.asScala.mkString(File.pathSeparator) + buffer ++= genClasspathOption(classpathEntries) + buffer += mainClass - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) - for ((k, v) <- conf.getAll) { - buffer += "--conf" - buffer += s"$k=$v" - } - buffer.toArray + buffer ++= confKeyValues(conf.getAll) + + buffer } override def toString: String = { if (commands == null) { - super.toString() + super.toString } else { - Utils.redactCommandLineArgs(conf, commands).map { - case arg if arg.contains(ENGINE_JDBC_CONNECTION_PASSWORD.key) => - s"${ENGINE_JDBC_CONNECTION_PASSWORD.key}=$REDACTION_REPLACEMENT_TEXT" + redactConfValues( + Utils.redactCommandLineArgs(conf, commands), + Set(ENGINE_JDBC_CONNECTION_PASSWORD.key)).map { + case arg if arg.startsWith("-") => s"\\\n\t$arg" case arg => arg - }.mkString("\n") + }.mkString(" ") } } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkBatchProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkBatchProcessBuilder.scala index ef159bb93ad..0167f95516d 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkBatchProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkBatchProcessBuilder.scala @@ -17,11 +17,12 @@ package org.apache.kyuubi.engine.spark -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.engine.KyuubiApplicationManager import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ class SparkBatchProcessBuilder( override val proxyUser: String, @@ -36,8 +37,8 @@ class SparkBatchProcessBuilder( extends SparkProcessBuilder(proxyUser, conf, batchId, extraEngineLog) { import SparkProcessBuilder._ - override protected lazy val commands: Array[String] = { - val buffer = new ArrayBuffer[String]() + override protected lazy val commands: Iterable[String] = { + val buffer = new mutable.ListBuffer[String]() buffer += executable Option(mainClass).foreach { cla => buffer += CLASS @@ -51,13 +52,11 @@ class SparkBatchProcessBuilder( // tag batch application KyuubiApplicationManager.tagApplication(batchId, "spark", clusterManager(), batchKyuubiConf) - (batchKyuubiConf.getAll ++ + val allConfigs = batchKyuubiConf.getAll ++ sparkAppNameConf() ++ engineLogPathConf() ++ - appendPodNameConf(batchConf)).foreach { case (k, v) => - buffer += CONF - buffer += s"${convertConfigKey(k)}=$v" - } + appendPodNameConf(batchConf) + buffer ++= confKeyValues(allConfigs) setupKerberos(buffer) @@ -66,7 +65,7 @@ class SparkBatchProcessBuilder( batchArgs.foreach { arg => buffer += arg } - buffer.toArray + buffer } private def sparkAppNameConf(): Map[String, String] = { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala index afc96fb5ea0..972284f5c06 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilder.scala @@ -22,7 +22,6 @@ import java.nio.file.Paths import java.util.Locale import scala.collection.mutable -import scala.collection.mutable.ArrayBuffer import com.google.common.annotations.VisibleForTesting import org.apache.commons.lang3.StringUtils @@ -30,6 +29,7 @@ import org.apache.hadoop.security.UserGroupInformation import org.apache.kyuubi._ import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.engine.{ApplicationManagerInfo, KyuubiApplicationManager, ProcBuilder} import org.apache.kyuubi.engine.KubernetesApplicationOperation.{KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT} import org.apache.kyuubi.engine.ProcBuilder.KYUUBI_ENGINE_LOG_PATH_KEY @@ -37,6 +37,7 @@ import org.apache.kyuubi.ha.HighAvailabilityConf import org.apache.kyuubi.ha.client.AuthTypes import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.util.{KubernetesUtils, Validator} +import org.apache.kyuubi.util.command.CommandLineUtils._ class SparkProcessBuilder( override val proxyUser: String, @@ -121,12 +122,12 @@ class SparkProcessBuilder( file.isDirectory && r.findFirstMatchIn(file.getName).isDefined } - override protected lazy val commands: Array[String] = { + override protected lazy val commands: Iterable[String] = { // complete `spark.master` if absent on kubernetes completeMasterUrl(conf) KyuubiApplicationManager.tagApplication(engineRefId, shortName, clusterManager(), conf) - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += executable buffer += CLASS buffer += mainClass @@ -139,21 +140,21 @@ class SparkProcessBuilder( allConf = allConf ++ zkAuthKeytabFileConf(allConf) } // pass spark engine log path to spark conf - (allConf ++ engineLogPathConf ++ appendPodNameConf(allConf)).foreach { case (k, v) => - buffer += CONF - buffer += s"${convertConfigKey(k)}=$v" + (allConf ++ engineLogPathConf ++ extraYarnConf(allConf) ++ appendPodNameConf(allConf)).foreach { + case (k, v) => + buffer ++= confKeyValue(convertConfigKey(k), v) } setupKerberos(buffer) mainResource.foreach { r => buffer += r } - buffer.toArray + buffer } override protected def module: String = "kyuubi-spark-sql-engine" - protected def setupKerberos(buffer: ArrayBuffer[String]): Unit = { + protected def setupKerberos(buffer: mutable.Buffer[String]): Unit = { // if the keytab is specified, PROXY_USER is not supported tryKeytab() match { case None => @@ -229,17 +230,28 @@ class SparkProcessBuilder( kubernetesNamespace()) } + private val forciblyRewriteDriverPodName: Boolean = + conf.get(KUBERNETES_FORCIBLY_REWRITE_DRIVER_POD_NAME) + private val forciblyRewriteExecPodNamePrefix: Boolean = + conf.get(KUBERNETES_FORCIBLY_REWRITE_EXEC_POD_NAME_PREFIX) + def appendPodNameConf(conf: Map[String, String]): Map[String, String] = { val appName = conf.getOrElse(APP_KEY, "spark") val map = mutable.Map.newBuilder[String, String] if (clusterManager().exists(cm => cm.toLowerCase(Locale.ROOT).startsWith("k8s"))) { if (!conf.contains(KUBERNETES_EXECUTOR_POD_NAME_PREFIX)) { - val prefix = KubernetesUtils.generateExecutorPodNamePrefix(appName, engineRefId) + val prefix = KubernetesUtils.generateExecutorPodNamePrefix( + appName, + engineRefId, + forciblyRewriteExecPodNamePrefix) map += (KUBERNETES_EXECUTOR_POD_NAME_PREFIX -> prefix) } if (deployMode().exists(_.toLowerCase(Locale.ROOT) == "cluster")) { if (!conf.contains(KUBERNETES_DRIVER_POD_NAME)) { - val name = KubernetesUtils.generateDriverPodName(appName, engineRefId) + val name = KubernetesUtils.generateDriverPodName( + appName, + engineRefId, + forciblyRewriteDriverPodName) map += (KUBERNETES_DRIVER_POD_NAME -> name) } } @@ -247,6 +259,18 @@ class SparkProcessBuilder( map.result().toMap } + def extraYarnConf(conf: Map[String, String]): Map[String, String] = { + val map = mutable.Map.newBuilder[String, String] + if (clusterManager().exists(_.toLowerCase(Locale.ROOT).startsWith("yarn"))) { + if (!conf.contains(YARN_MAX_APP_ATTEMPTS_KEY)) { + // Set `spark.yarn.maxAppAttempts` to 1 to avoid invalid attempts. + // As mentioned in YARN-5617, it is improved after hadoop `2.8.2/2.9.0/3.0.0`. + map += (YARN_MAX_APP_ATTEMPTS_KEY -> "1") + } + } + map.result().toMap + } + override def clusterManager(): Option[String] = { conf.getOption(MASTER_KEY).orElse(defaultsConf.get(MASTER_KEY)) } @@ -274,13 +298,11 @@ class SparkProcessBuilder( override def validateConf: Unit = Validator.validateConf(conf) // For spark on kubernetes, spark pod using env SPARK_USER_NAME as current user - def setSparkUserName(userName: String, buffer: ArrayBuffer[String]): Unit = { + def setSparkUserName(userName: String, buffer: mutable.Buffer[String]): Unit = { clusterManager().foreach { cm => if (cm.toUpperCase.startsWith("K8S")) { - buffer += CONF - buffer += s"spark.kubernetes.driverEnv.SPARK_USER_NAME=$userName" - buffer += CONF - buffer += s"spark.executorEnv.SPARK_USER_NAME=$userName" + buffer ++= confKeyValue("spark.kubernetes.driverEnv.SPARK_USER_NAME", userName) + buffer ++= confKeyValue("spark.executorEnv.SPARK_USER_NAME", userName) } } } @@ -299,6 +321,7 @@ object SparkProcessBuilder { final val KUBERNETES_NAMESPACE_KEY = "spark.kubernetes.namespace" final val KUBERNETES_DRIVER_POD_NAME = "spark.kubernetes.driver.pod.name" final val KUBERNETES_EXECUTOR_POD_NAME_PREFIX = "spark.kubernetes.executor.podNamePrefix" + final val YARN_MAX_APP_ATTEMPTS_KEY = "spark.yarn.maxAppAttempts" final val INTERNAL_RESOURCE = "spark-internal" /** @@ -323,7 +346,6 @@ object SparkProcessBuilder { "spark.kubernetes.kerberos.krb5.path", "spark.kubernetes.file.upload.path") - final private[spark] val CONF = "--conf" final private[spark] val CLASS = "--class" final private[spark] val PROXY_USER = "--proxy-user" final private[spark] val SPARK_FILES = "spark.files" diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala index 041219dd0fb..96502fb9607 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala @@ -19,20 +19,18 @@ package org.apache.kyuubi.engine.trino import java.io.File import java.nio.file.Paths -import java.util -import scala.collection.JavaConverters._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable import com.google.common.annotations.VisibleForTesting import org.apache.kyuubi.{Logging, SCALA_COMPILE_VERSION, Utils} -import org.apache.kyuubi.Utils.REDACTION_REPLACEMENT_TEXT import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_USER_KEY import org.apache.kyuubi.engine.{KyuubiApplicationManager, ProcBuilder} import org.apache.kyuubi.operation.log.OperationLog +import org.apache.kyuubi.util.command.CommandLineUtils._ class TrinoProcessBuilder( override val proxyUser: String, @@ -50,7 +48,7 @@ class TrinoProcessBuilder( override protected def mainClass: String = "org.apache.kyuubi.engine.trino.TrinoSqlEngine" - override protected val commands: Array[String] = { + override protected val commands: Iterable[String] = { KyuubiApplicationManager.tagApplication(engineRefId, shortName, clusterManager(), conf) require( conf.get(ENGINE_TRINO_CONNECTION_URL).nonEmpty, @@ -58,7 +56,7 @@ class TrinoProcessBuilder( require( conf.get(ENGINE_TRINO_CONNECTION_CATALOG).nonEmpty, s"Trino default catalog can not be null! Please set ${ENGINE_TRINO_CONNECTION_CATALOG.key}") - val buffer = new ArrayBuffer[String]() + val buffer = new mutable.ListBuffer[String]() buffer += executable val memory = conf.get(ENGINE_TRINO_MEMORY) @@ -68,8 +66,7 @@ class TrinoProcessBuilder( buffer += javaOptions.get } - buffer += "-cp" - val classpathEntries = new util.LinkedHashSet[String] + val classpathEntries = new mutable.LinkedHashSet[String] // trino engine runtime jar mainResource.foreach(classpathEntries.add) @@ -90,38 +87,36 @@ class TrinoProcessBuilder( val extraCp = conf.get(ENGINE_TRINO_EXTRA_CLASSPATH) extraCp.foreach(classpathEntries.add) - buffer += classpathEntries.asScala.mkString(File.pathSeparator) + buffer ++= genClasspathOption(classpathEntries) + buffer += mainClass // TODO: How shall we deal with proxyUser, // user.name // kyuubi.session.user // or just leave it, because we can handle it at operation layer - buffer += "--conf" - buffer += s"$KYUUBI_SESSION_USER_KEY=$proxyUser" + buffer ++= confKeyValue(KYUUBI_SESSION_USER_KEY, proxyUser) - for ((k, v) <- conf.getAll) { - buffer += "--conf" - buffer += s"$k=$v" - } - buffer.toArray + buffer ++= confKeyValues(conf.getAll) + + buffer } override def shortName: String = "trino" override def toString: String = { if (commands == null) { - super.toString() + super.toString } else { - Utils.redactCommandLineArgs(conf, commands).map { - case arg if arg.contains(ENGINE_TRINO_CONNECTION_PASSWORD.key) => - s"${ENGINE_TRINO_CONNECTION_PASSWORD.key}=$REDACTION_REPLACEMENT_TEXT" - case arg if arg.contains(ENGINE_TRINO_CONNECTION_KEYSTORE_PASSWORD.key) => - s"${ENGINE_TRINO_CONNECTION_KEYSTORE_PASSWORD.key}=$REDACTION_REPLACEMENT_TEXT" - case arg if arg.contains(ENGINE_TRINO_CONNECTION_TRUSTSTORE_PASSWORD.key) => - s"${ENGINE_TRINO_CONNECTION_TRUSTSTORE_PASSWORD.key}=$REDACTION_REPLACEMENT_TEXT" + redactConfValues( + Utils.redactCommandLineArgs(conf, commands), + Set( + ENGINE_TRINO_CONNECTION_PASSWORD.key, + ENGINE_TRINO_CONNECTION_KEYSTORE_PASSWORD.key, + ENGINE_TRINO_CONNECTION_TRUSTSTORE_PASSWORD.key)).map { + case arg if arg.startsWith("-") => s"\\\n\t$arg" case arg => arg - }.mkString("\n") + }.mkString(" ") } } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/BatchJobSubmission.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/BatchJobSubmission.scala index af6242ae1c7..276fe344600 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/BatchJobSubmission.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/BatchJobSubmission.scala @@ -17,14 +17,12 @@ package org.apache.kyuubi.operation -import java.io.IOException import java.nio.file.{Files, Paths} import java.util.Locale import java.util.concurrent.TimeUnit import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting -import org.apache.hive.service.rpc.thrift._ import org.apache.kyuubi.{KyuubiException, KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiConf @@ -37,6 +35,7 @@ import org.apache.kyuubi.operation.OperationState.{isTerminal, CANCELED, Operati import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.server.metadata.api.Metadata import org.apache.kyuubi.session.KyuubiBatchSession +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ /** * The state of batch operation is special. In general, the lifecycle of state is: @@ -182,7 +181,7 @@ class BatchJobSubmission( OperationLog.removeCurrentOperationLog() } - override protected def runInternal(): Unit = session.handleSessionException { + override protected def runInternal(): Unit = { val asyncOperation: Runnable = () => { try { metadata match { @@ -336,14 +335,8 @@ class BatchJobSubmission( } } - override def close(): Unit = withLockRequired { + override def close(): Unit = withLockRequired(withClosingOperationLog { if (!isClosedOrCanceled) { - try { - getOperationLog.foreach(_.close()) - } catch { - case e: IOException => error(e.getMessage, e) - } - MetricsSystem.tracing(_.decCount(MetricRegistry.name(OPERATION_OPEN, opType))) // fast fail @@ -379,7 +372,7 @@ class BatchJobSubmission( } } } - } + }) override def cancel(): Unit = { throw new IllegalStateException("Use close instead.") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecuteStatement.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecuteStatement.scala index 86bd3f8c84c..026d4be2ddb 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecuteStatement.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecuteStatement.scala @@ -20,8 +20,6 @@ package org.apache.kyuubi.operation import scala.collection.JavaConverters._ import com.codahale.metrics.MetricRegistry -import org.apache.hive.service.rpc.thrift.{TGetOperationStatusResp, TOperationState, TProtocolVersion} -import org.apache.hive.service.rpc.thrift.TOperationState._ import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf @@ -30,6 +28,8 @@ import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.operation.FetchOrientation.FETCH_NEXT import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetOperationStatusResp, TOperationState, TProtocolVersion} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TOperationState._ class ExecuteStatement( session: Session, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecutedCommandExec.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecutedCommandExec.scala index 70b727e5e67..a59c2db7b77 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecutedCommandExec.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/ExecutedCommandExec.scala @@ -17,11 +17,10 @@ package org.apache.kyuubi.operation -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} - import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.session.KyuubiSessionImpl +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TGetResultSetMetadataResp} import org.apache.kyuubi.sql.plan.command.RunnableCommand import org.apache.kyuubi.sql.schema.SchemaHelper @@ -49,7 +48,7 @@ class ExecutedCommandExec( OperationLog.removeCurrentOperationLog() } - override protected def runInternal(): Unit = session.handleSessionException { + override protected def runInternal(): Unit = { val asyncOperation: Runnable = () => { setState(OperationState.RUNNING) try { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiApplicationOperation.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiApplicationOperation.scala index 93929c59cce..1ef70f266f3 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiApplicationOperation.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiApplicationOperation.scala @@ -22,11 +22,10 @@ import java.util.{ArrayList => JArrayList} import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TColumn, TColumnDesc, TFetchResultsResp, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TRow, TRowSet, TStringColumn, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} - import org.apache.kyuubi.engine.ApplicationInfo import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.session.Session +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TColumn, TColumnDesc, TFetchResultsResp, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TRow, TRowSet, TStringColumn, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} import org.apache.kyuubi.util.ThriftUtils abstract class KyuubiApplicationOperation(session: Session) extends KyuubiOperation(session) { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperation.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperation.scala index 83e19cb6579..54a7c96029a 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperation.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperation.scala @@ -21,9 +21,6 @@ import java.io.IOException import com.codahale.metrics.MetricRegistry import org.apache.commons.lang3.StringUtils -import org.apache.hive.service.rpc.thrift._ -import org.apache.thrift.TException -import org.apache.thrift.transport.TTransportException import org.apache.kyuubi.{KyuubiSQLException, Utils} import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_OPERATION_HANDLE_KEY @@ -32,7 +29,10 @@ import org.apache.kyuubi.metrics.MetricsConstants.{OPERATION_FAIL, OPERATION_OPE import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.operation.OperationState.OperationState -import org.apache.kyuubi.session.{KyuubiSessionImpl, KyuubiSessionManager, Session} +import org.apache.kyuubi.session.{KyuubiSession, KyuubiSessionImpl, KyuubiSessionManager, Session} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.TException +import org.apache.kyuubi.shaded.thrift.transport.TTransportException import org.apache.kyuubi.util.ThriftUtils abstract class KyuubiOperation(session: Session) extends AbstractOperation(session) { @@ -100,6 +100,17 @@ abstract class KyuubiOperation(session: Session) extends AbstractOperation(sessi } } + override def run(): Unit = { + beforeRun() + try { + session.asInstanceOf[KyuubiSession].handleSessionException { + runInternal() + } + } finally { + afterRun() + } + } + override protected def beforeRun(): Unit = { setHasResultSet(true) setState(OperationState.RUNNING) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperationManager.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperationManager.scala index 739c99cd78a..a248fe2a832 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperationManager.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/KyuubiOperationManager.scala @@ -19,8 +19,6 @@ package org.apache.kyuubi.operation import java.util.concurrent.TimeUnit -import org.apache.hive.service.rpc.thrift.{TFetchResultsResp, TStatus, TStatusCode} - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.OPERATION_QUERY_TIMEOUT @@ -29,6 +27,7 @@ import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.server.metadata.api.Metadata import org.apache.kyuubi.session.{KyuubiBatchSession, KyuubiSessionImpl, Session} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TFetchResultsResp, TStatus, TStatusCode} import org.apache.kyuubi.sql.plan.command.RunnableCommand import org.apache.kyuubi.util.ThriftUtils diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/LaunchEngine.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/LaunchEngine.scala index 758dccb9d1b..cfbd2a0ca9a 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/LaunchEngine.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/operation/LaunchEngine.scala @@ -53,7 +53,7 @@ class LaunchEngine(session: KyuubiSessionImpl, override val shouldRunAsync: Bool OperationLog.removeCurrentOperationLog() } - override protected def runInternal(): Unit = session.handleSessionException { + override protected def runInternal(): Unit = { val asyncOperation: Runnable = () => { setState(OperationState.RUNNING) try { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala index da4c8e4a9d1..1bc80dc7da1 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala @@ -25,20 +25,22 @@ import org.apache.kyuubi.util.reflect.DynConstructors private[kyuubi] object PluginLoader { - def loadSessionConfAdvisor(conf: KyuubiConf): SessionConfAdvisor = { + def loadSessionConfAdvisor(conf: KyuubiConf): Seq[SessionConfAdvisor] = { val advisorClass = conf.get(KyuubiConf.SESSION_CONF_ADVISOR) if (advisorClass.isEmpty) { - return new DefaultSessionConfAdvisor() + return new DefaultSessionConfAdvisor() :: Nil } - - try { - DynConstructors.builder.impl(advisorClass.get).buildChecked[SessionConfAdvisor].newInstance() - } catch { - case _: ClassCastException => - throw new KyuubiException( - s"Class ${advisorClass.get} is not a child of '${classOf[SessionConfAdvisor].getName}'.") - case NonFatal(e) => - throw new IllegalArgumentException(s"Error while instantiating '${advisorClass.get}': ", e) + advisorClass.get.map { advisorClassName => + try { + DynConstructors.builder.impl(advisorClassName) + .buildChecked[SessionConfAdvisor].newInstance() + } catch { + case _: ClassCastException => + throw new KyuubiException( + s"Class $advisorClassName is not a child of '${classOf[SessionConfAdvisor].getName}'.") + case NonFatal(e) => + throw new IllegalArgumentException(s"Error while instantiating '$advisorClassName': ", e) + } } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/BackendServiceMetric.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/BackendServiceMetric.scala index 9da4b78c036..4b4ab6f56c0 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/BackendServiceMetric.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/BackendServiceMetric.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.server -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle, OperationStatus} import org.apache.kyuubi.operation.FetchOrientation.FetchOrientation import org.apache.kyuubi.service.BackendService import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ trait BackendServiceMetric extends BackendService { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala index c5d44213c90..d738995130b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala @@ -35,9 +35,10 @@ import org.apache.kyuubi.server.api.v1.ApiRootResource import org.apache.kyuubi.server.http.authentication.{AuthenticationFilter, KyuubiHttpAuthenticationFactory} import org.apache.kyuubi.server.ui.{JettyServer, JettyUtils} import org.apache.kyuubi.service.{AbstractFrontendService, Serverable, Service, ServiceUtils} -import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory +import org.apache.kyuubi.service.authentication.{AuthMethods, AuthTypes, KyuubiAuthenticationFactory} import org.apache.kyuubi.session.{KyuubiSessionManager, SessionHandle} import org.apache.kyuubi.util.ThreadUtils +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay /** * A frontend service based on RESTful api via HTTP protocol. @@ -70,6 +71,17 @@ class KyuubiRestFrontendService(override val serverable: Serverable) private lazy val port: Int = conf.get(FRONTEND_REST_BIND_PORT) + private lazy val securityEnabled = { + val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + KyuubiAuthenticationFactory.getValidPasswordAuthMethod(authTypes) != AuthMethods.NONE + } + + private lazy val administrators: Set[String] = + conf.get(KyuubiConf.SERVER_ADMINISTRATORS) + Utils.currentUser + + def isAdministrator(userName: String): Boolean = + if (securityEnabled) administrators.contains(userName) else true + override def initialize(conf: KyuubiConf): Unit = synchronized { this.conf = conf server = JettyServer( @@ -131,7 +143,12 @@ class KyuubiRestFrontendService(override val serverable: Serverable) } } - batchChecker.scheduleWithFixedDelay(task, interval, interval, TimeUnit.MILLISECONDS) + scheduleTolerableRunnableWithFixedDelay( + batchChecker, + task, + interval, + interval, + TimeUnit.MILLISECONDS) } @VisibleForTesting @@ -208,9 +225,10 @@ class KyuubiRestFrontendService(override val serverable: Serverable) Option(AuthenticationFilter.getUserName).filter(_.nonEmpty).getOrElse("anonymous")) } - def getSessionUser(hs2ProxyUser: String): String = { - val sessionConf = Option(hs2ProxyUser).filter(_.nonEmpty).map(proxyUser => - Map(KyuubiAuthenticationFactory.HS2_PROXY_USER -> proxyUser)).getOrElse(Map()) + def getSessionUser(proxyUser: String): String = { + // Internally, we use kyuubi.session.proxy.user to unify the key as proxyUser + val sessionConf = Option(proxyUser).filter(_.nonEmpty).map(proxyUser => + Map(PROXY_USER.key -> proxyUser)).getOrElse(Map()) getSessionUser(sessionConf) } @@ -239,12 +257,13 @@ class KyuubiRestFrontendService(override val serverable: Serverable) if (sessionConf == null) { realUser } else { - sessionConf.get(KyuubiAuthenticationFactory.HS2_PROXY_USER).map { proxyUser => - if (!getConf.get(KyuubiConf.SERVER_ADMINISTRATORS).contains(realUser)) { - KyuubiAuthenticationFactory.verifyProxyAccess(realUser, proxyUser, ipAddress, hadoopConf) - } - proxyUser - }.getOrElse(realUser) + val proxyUser = sessionConf.getOrElse( + PROXY_USER.key, + sessionConf.getOrElse(KyuubiAuthenticationFactory.HS2_PROXY_USER, realUser)) + if (!proxyUser.equals(realUser) && !isAdministrator(realUser)) { + KyuubiAuthenticationFactory.verifyProxyAccess(realUser, proxyUser, ipAddress, hadoopConf) + } + proxyUser } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala index ae388a7c42a..b46c1fec4e2 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendService.scala @@ -20,9 +20,6 @@ package org.apache.kyuubi.server import java.util.Base64 import org.apache.hadoop.conf.Configuration -import org.apache.hive.service.rpc.thrift._ -import org.apache.thrift.protocol.TProtocol -import org.apache.thrift.server.ServerContext import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.cli.Handle @@ -34,6 +31,9 @@ import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.service.{Serverable, Service, TBinaryFrontendService} import org.apache.kyuubi.service.TFrontendService.{CURRENT_SERVER_CONTEXT, FeServiceServerContext, OK_STATUS} import org.apache.kyuubi.session.KyuubiSessionImpl +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.protocol.TProtocol +import org.apache.kyuubi.shaded.thrift.server.ServerContext final class KyuubiTBinaryFrontendService( override val serverable: Serverable) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala index 79351118c50..ca8939d69a3 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala @@ -24,8 +24,6 @@ import javax.servlet.{ServletContextEvent, ServletContextListener} import org.apache.commons.lang3.SystemUtils import org.apache.hadoop.conf.Configuration -import org.apache.hive.service.rpc.thrift.{TCLIService, TOpenSessionReq} -import org.apache.thrift.protocol.TBinaryProtocol import org.eclipse.jetty.http.HttpMethod import org.eclipse.jetty.security.{ConstraintMapping, ConstraintSecurityHandler} import org.eclipse.jetty.server._ @@ -43,6 +41,9 @@ import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.server.http.ThriftHttpServlet import org.apache.kyuubi.server.http.util.SessionManager import org.apache.kyuubi.service.{Serverable, Service, ServiceUtils, TFrontendService} +import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TCLIService, TOpenSessionReq} +import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol import org.apache.kyuubi.util.NamedThreadFactory /** @@ -74,9 +75,9 @@ final class KyuubiTHttpFrontendService( */ override def initialize(conf: KyuubiConf): Unit = synchronized { this.conf = conf - if (authFactory.isKerberosEnabled) { + if (authFactory.kerberosEnabled) { try { - authFactory.getValidPasswordAuthMethod + KyuubiAuthenticationFactory.getValidPasswordAuthMethod(authFactory.authTypes) } catch { case _: IllegalArgumentException => throw new AuthenticationException("Kerberos is not supported for thrift http mode") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/PeriodicGCService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/PeriodicGCService.scala index a4035b689d5..4ec6f4c127e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/PeriodicGCService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/PeriodicGCService.scala @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.service.AbstractService import org.apache.kyuubi.util.ThreadUtils +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay class PeriodicGCService(name: String) extends AbstractService(name) { def this() = this(classOf[PeriodicGCService].getSimpleName) @@ -40,6 +41,11 @@ class PeriodicGCService(name: String) extends AbstractService(name) { private def startGcTrigger(): Unit = { val interval = conf.get(KyuubiConf.SERVER_PERIODIC_GC_INTERVAL) - gcTrigger.scheduleWithFixedDelay(() => System.gc(), interval, interval, TimeUnit.MILLISECONDS) + scheduleTolerableRunnableWithFixedDelay( + gcTrigger, + () => System.gc(), + interval, + interval, + TimeUnit.MILLISECONDS) } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 5aaf4d7780f..49442160878 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -20,18 +20,41 @@ package org.apache.kyuubi.server.api import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} -import org.apache.kyuubi.client.api.v1.dto.{OperationData, ServerData, SessionData} +import org.apache.kyuubi.client.api.v1.dto +import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} import org.apache.kyuubi.events.KyuubiOperationEvent import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession object ApiUtils extends Logging { + def sessionEvent(session: KyuubiSession): dto.KyuubiSessionEvent = { + session.getSessionEvent.map(event => + dto.KyuubiSessionEvent.builder() + .sessionId(event.sessionId) + .clientVersion(event.clientVersion) + .sessionType(event.sessionType) + .sessionName(event.sessionName) + .user(event.user) + .clientIp(event.clientIP) + .serverIp(event.serverIP) + .conf(event.conf.asJava) + .remoteSessionId(event.remoteSessionId) + .engineId(event.engineId) + .eventTime(event.eventTime) + .openedTime(event.openedTime) + .startTime(event.startTime) + .endTime(event.endTime) + .totalOperations(event.totalOperations) + .exception(event.exception.orNull) + .build()).orNull + } def sessionData(session: KyuubiSession): SessionData = { val sessionEvent = session.getSessionEvent new SessionData( session.handle.identifier.toString, + sessionEvent.map(_.remoteSessionId).getOrElse(""), session.user, session.ipAddress, session.conf.asJava, @@ -44,10 +67,45 @@ object ApiUtils extends Logging { sessionEvent.map(_.engineId).getOrElse("")) } + private def operationProgress(operation: KyuubiOperation): OperationProgress = { + Option(operation.getOperationJobProgress).map { jobProgress => + new OperationProgress( + jobProgress.getHeaderNames, + jobProgress.getRows, + jobProgress.getProgressedPercentage, + jobProgress.getStatus.toString, + jobProgress.getFooterSummary, + jobProgress.getStartTime) + }.orNull + } + + def operationEvent(operation: KyuubiOperation): dto.KyuubiOperationEvent = { + val opEvent = KyuubiOperationEvent(operation) + dto.KyuubiOperationEvent.builder() + .statementId(opEvent.statementId) + .remoteId(opEvent.remoteId) + .statement(opEvent.statement) + .shouldRunAsync(opEvent.shouldRunAsync) + .state(opEvent.state) + .eventTime(opEvent.eventTime) + .createTime(opEvent.createTime) + .startTime(opEvent.startTime) + .completeTime(opEvent.completeTime) + .exception(opEvent.exception.orNull) + .sessionId(opEvent.sessionId) + .sessionUser(opEvent.sessionUser) + .sessionType(opEvent.sessionType) + .kyuubiInstance(opEvent.kyuubiInstance) + .metrics(opEvent.metrics.asJava) + .progress(operationProgress(operation)) + .build() + } + def operationData(operation: KyuubiOperation): OperationData = { val opEvent = KyuubiOperationEvent(operation) new OperationData( opEvent.statementId, + opEvent.remoteId, opEvent.statement, opEvent.state, opEvent.createTime, @@ -58,7 +116,8 @@ object ApiUtils extends Logging { opEvent.sessionUser, opEvent.sessionType, operation.getSession.asInstanceOf[KyuubiSession].connectionUrl, - operation.metrics.asJava) + operation.metrics.asJava, + operationProgress(operation)) } def serverData(nodeInfo: ServiceNodeInfo): ServerData = { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala index 3c6f2a19782..ca35a1b6b12 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala @@ -29,7 +29,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import org.apache.commons.lang3.StringUtils -import org.apache.kyuubi.{KYUUBI_VERSION, Logging, Utils} +import org.apache.kyuubi.{KYUUBI_VERSION, Logging} import org.apache.kyuubi.client.api.v1.dto._ import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ @@ -40,13 +40,10 @@ import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle} import org.apache.kyuubi.server.KyuubiServer import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils} import org.apache.kyuubi.session.{KyuubiSession, SessionHandle} -import org.apache.kyuubi.shaded.zookeeper.KeeperException.NoNodeException @Tag(name = "Admin") @Produces(Array(MediaType.APPLICATION_JSON)) private[v1] class AdminResource extends ApiRequestContext with Logging { - private lazy val administrators = fe.getConf.get(KyuubiConf.SERVER_ADMINISTRATORS) + - Utils.currentUser @ApiResponse( responseCode = "200", @@ -59,7 +56,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Receive refresh Kyuubi server hadoop conf request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to refresh the Kyuubi server hadoop conf") } @@ -78,7 +75,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Receive refresh user defaults conf request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to refresh the user defaults conf") } @@ -97,7 +94,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Receive refresh kubernetes conf request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to refresh the kubernetes conf") } @@ -116,7 +113,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Receive refresh unlimited users request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to refresh the unlimited users") } @@ -135,7 +132,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Receive refresh deny users request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to refresh the deny users") } @@ -156,7 +153,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received listing all live sessions request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to list all live sessions") } @@ -178,7 +175,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received closing a session request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to close the session $sessionHandleStr") } @@ -202,7 +199,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received listing all of the active operations request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to list all the operations") } @@ -229,7 +226,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received close an operation request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to close the operation $operationHandleStr") } @@ -248,14 +245,16 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { @QueryParam("type") engineType: String, @QueryParam("sharelevel") shareLevel: String, @QueryParam("subdomain") subdomain: String, + @QueryParam("proxyUser") kyuubiProxyUser: String, @QueryParam("hive.server2.proxy.user") hs2ProxyUser: String): Response = { - val userName = if (isAdministrator(fe.getRealUser())) { - Option(hs2ProxyUser).getOrElse(fe.getRealUser()) + val activeProxyUser = Option(kyuubiProxyUser).getOrElse(hs2ProxyUser) + val userName = if (fe.isAdministrator(fe.getRealUser())) { + Option(activeProxyUser).getOrElse(fe.getRealUser()) } else { - fe.getSessionUser(hs2ProxyUser) + fe.getSessionUser(activeProxyUser) } - val engine = getEngine(userName, engineType, shareLevel, subdomain, "default") - val engineSpace = getEngineSpace(engine) + val engine = normalizeEngineInfo(userName, engineType, shareLevel, subdomain, "default") + val engineSpace = calculateEngineSpace(engine) withDiscoveryClient(fe.getConf) { discoveryClient => val engineNodes = discoveryClient.getChildren(engineSpace) @@ -286,86 +285,32 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { @QueryParam("type") engineType: String, @QueryParam("sharelevel") shareLevel: String, @QueryParam("subdomain") subdomain: String, - @QueryParam("hive.server2.proxy.user") hs2ProxyUser: String, - @QueryParam("all") @DefaultValue("false") all: String): Seq[Engine] = { - if (all.toBoolean) { - val userName = fe.getSessionUser(Map.empty[String, String]) - val ipAddress = fe.getIpAddress - info(s"Received list all kyuubi engine request from $userName/$ipAddress") - if (!isAdministrator(userName)) { - throw new NotAllowedException( - s"$userName is not allowed to list all kyuubi engine") - } - val engines = ListBuffer[Engine]() - val engineSpace = fe.getConf.get(HA_NAMESPACE) - val finalShareLevel = Option(shareLevel).getOrElse(fe.getConf.get(ENGINE_SHARE_LEVEL)) - val finalEngineType = Option(engineType).getOrElse(fe.getConf.get(ENGINE_TYPE)) - withDiscoveryClient(fe.getConf) { discoveryClient => - val commonParent = s"/${engineSpace}_${KYUUBI_VERSION}_${finalShareLevel}_$finalEngineType" - info(s"Listing engine nodes for $commonParent") - try { - discoveryClient.getChildren(commonParent).map { - user => - val engine = getEngine(user, finalEngineType, finalShareLevel, "", "") - val engineSpace = getEngineSpace(engine) - discoveryClient.getChildren(engineSpace).map { child => - info(s"Listing engine nodes for $engineSpace/$child") - engines ++= discoveryClient.getServiceNodesInfo(s"$engineSpace/$child").map(node => - new Engine( - engine.getVersion, - engine.getUser, - engine.getEngineType, - engine.getSharelevel, - node.namespace.split("/").last, - node.instance, - node.namespace, - node.attributes.asJava)) - } - } - } catch { - case nne: NoNodeException => - error( - s"No such engine for engine type: $finalEngineType," + - s" share level: $finalShareLevel", - nne) - throw new NotFoundException( - s"No such engine for engine type: $finalEngineType, share level: $finalShareLevel") - } - } - return engines.toSeq - } - val userName = if (isAdministrator(fe.getRealUser())) { - Option(hs2ProxyUser).getOrElse(fe.getRealUser()) + @QueryParam("proxyUser") kyuubiProxyUser: String, + @QueryParam("hive.server2.proxy.user") hs2ProxyUser: String): Seq[Engine] = { + val activeProxyUser = Option(kyuubiProxyUser).getOrElse(hs2ProxyUser) + val userName = if (fe.isAdministrator(fe.getRealUser())) { + Option(activeProxyUser).getOrElse(fe.getRealUser()) } else { - fe.getSessionUser(hs2ProxyUser) + fe.getSessionUser(activeProxyUser) } - val engine = getEngine(userName, engineType, shareLevel, subdomain, "") - val engineSpace = getEngineSpace(engine) + val engine = normalizeEngineInfo(userName, engineType, shareLevel, subdomain, "") + val engineSpace = calculateEngineSpace(engine) val engineNodes = ListBuffer[ServiceNodeInfo]() - Option(subdomain).filter(_.nonEmpty) match { - case Some(_) => - withDiscoveryClient(fe.getConf) { discoveryClient => - info(s"Listing engine nodes for $engineSpace") + withDiscoveryClient(fe.getConf) { discoveryClient => + Option(subdomain).filter(_.nonEmpty) match { + case Some(_) => + info(s"Listing engine nodes under $engineSpace") engineNodes ++= discoveryClient.getServiceNodesInfo(engineSpace) - } - case None => - withDiscoveryClient(fe.getConf) { discoveryClient => - try { - discoveryClient.getChildren(engineSpace).map { child => - info(s"Listing engine nodes for $engineSpace/$child") - engineNodes ++= discoveryClient.getServiceNodesInfo(s"$engineSpace/$child") - } - } catch { - case nne: NoNodeException => - error( - s"No such engine for user: $userName, " + - s"engine type: $engineType, share level: $shareLevel, subdomain: $subdomain", - nne) - throw new NotFoundException(s"No such engine for user: $userName, " + - s"engine type: $engineType, share level: $shareLevel, subdomain: $subdomain") + case None if discoveryClient.pathNonExists(engineSpace) => + warn(s"Path $engineSpace does not exist. user: $userName, engine type: $engineType, " + + s"share level: $shareLevel, subdomain: $subdomain") + case None => + discoveryClient.getChildren(engineSpace).map { child => + info(s"Listing engine nodes under $engineSpace/$child") + engineNodes ++= discoveryClient.getServiceNodesInfo(s"$engineSpace/$child") } - } + } } engineNodes.map(node => new Engine( @@ -394,7 +339,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received list all live kyuubi servers request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to list all live kyuubi servers") } @@ -409,7 +354,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { servers.toSeq } - private def getEngine( + private def normalizeEngineInfo( userName: String, engineType: String, shareLevel: String, @@ -422,6 +367,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { .foreach(_ => clonedConf.set(ENGINE_SHARE_LEVEL_SUBDOMAIN, Option(subdomain))) Option(shareLevel).filter(_.nonEmpty).foreach(clonedConf.set(ENGINE_SHARE_LEVEL, _)) + val serverSpace = clonedConf.get(HA_NAMESPACE) val normalizedEngineType = clonedConf.get(ENGINE_TYPE) val engineSubdomain = clonedConf.get(ENGINE_SHARE_LEVEL_SUBDOMAIN).getOrElse(subdomainDefault) val engineShareLevel = clonedConf.get(ENGINE_SHARE_LEVEL) @@ -433,22 +379,20 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { engineShareLevel, engineSubdomain, null, - null, + serverSpace, Collections.emptyMap()) } - private def getEngineSpace(engine: Engine): String = { - val serverSpace = fe.getConf.get(HA_NAMESPACE) - val appUser = engine.getSharelevel match { + private def calculateEngineSpace(engine: Engine): String = { + val userOrGroup = engine.getSharelevel match { case "GROUP" => fe.sessionManager.groupProvider.primaryGroup(engine.getUser, fe.getConf.getAll.asJava) case _ => engine.getUser } - DiscoveryPaths.makePath( - s"${serverSpace}_${engine.getVersion}_${engine.getSharelevel}_${engine.getEngineType}", - appUser, - engine.getSubdomain) + val engineSpace = + s"${engine.getNamespace}_${engine.getVersion}_${engine.getSharelevel}_${engine.getEngineType}" + DiscoveryPaths.makePath(engineSpace, userOrGroup, engine.getSubdomain) } @ApiResponse( @@ -466,7 +410,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { val userName = fe.getSessionUser(Map.empty[String, String]) val ipAddress = fe.getIpAddress info(s"Received counting batches request from $userName/$ipAddress") - if (!isAdministrator(userName)) { + if (!fe.isAdministrator(userName)) { throw new NotAllowedException( s"$userName is not allowed to count the batches") } @@ -475,8 +419,4 @@ private[v1] class AdminResource extends ApiRequestContext with Logging { .getOrElse(0) new Count(batchCount) } - - private def isAdministrator(userName: String): Boolean = { - administrators.contains(userName) - } } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/BatchesResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/BatchesResource.scala index c0a3b0ed905..4e3f8d20b03 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/BatchesResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/BatchesResource.scala @@ -23,7 +23,6 @@ import java.util.{Collections, Locale, UUID} import java.util.concurrent.ConcurrentHashMap import javax.ws.rs._ import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response.Status import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} @@ -58,6 +57,8 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { fe.getConf.get(BATCH_INTERNAL_REST_CLIENT_SOCKET_TIMEOUT).toInt private lazy val internalConnectTimeout = fe.getConf.get(BATCH_INTERNAL_REST_CLIENT_CONNECT_TIMEOUT).toInt + private lazy val internalSecurityEnabled = + fe.getConf.get(ENGINE_SECURITY_ENABLED) private def batchV2Enabled(reqConf: Map[String, String]): Boolean = { KyuubiServer.kyuubiServer.getConf.get(BATCH_SUBMITTER_ENABLED) && @@ -67,7 +68,12 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { private def getInternalRestClient(kyuubiInstance: String): InternalRestClient = { internalRestClients.computeIfAbsent( kyuubiInstance, - k => new InternalRestClient(k, internalSocketTimeout, internalConnectTimeout)) + kyuubiInstance => + new InternalRestClient( + kyuubiInstance, + internalSocketTimeout, + internalConnectTimeout, + internalSecurityEnabled)) } private def sessionManager = fe.be.sessionManager.asInstanceOf[KyuubiSessionManager] @@ -212,6 +218,8 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { } request.setBatchType(request.getBatchType.toUpperCase(Locale.ROOT)) + val userName = fe.getSessionUser(request.getConf.asScala.toMap) + val ipAddress = fe.getIpAddress val userProvidedBatchId = request.getConf.asScala.get(KYUUBI_BATCH_ID_KEY) userProvidedBatchId.foreach { batchId => try UUID.fromString(batchId) @@ -227,8 +235,6 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { case Some(batch) => markDuplicated(batch) case None => - val userName = fe.getSessionUser(request.getConf.asScala.toMap) - val ipAddress = fe.getIpAddress val batchId = userProvidedBatchId.getOrElse(UUID.randomUUID().toString) request.setConf( (request.getConf.asScala ++ Map( @@ -441,18 +447,7 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { description = "close and cancel a batch session") @DELETE @Path("{batchId}") - def closeBatchSession( - @PathParam("batchId") batchId: String, - @QueryParam("hive.server2.proxy.user") hs2ProxyUser: String): CloseBatchResponse = { - - def checkPermission(operator: String, owner: String): Unit = { - if (operator != owner) { - throw new WebApplicationException( - s"$operator is not allowed to close the session belong to $owner", - Status.METHOD_NOT_ALLOWED) - } - } - + def closeBatchSession(@PathParam("batchId") batchId: String): CloseBatchResponse = { def forceKill( appMgrInfo: ApplicationManagerInfo, batchId: String, @@ -465,16 +460,14 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { } val sessionHandle = formatSessionHandle(batchId) - val userName = fe.getSessionUser(hs2ProxyUser) - sessionManager.getBatchSession(sessionHandle).map { batchSession => - checkPermission(userName, batchSession.user) + fe.getSessionUser(batchSession.user) sessionManager.closeSession(batchSession.handle) val (killed, msg) = batchSession.batchJobSubmissionOp.getKillMessage new CloseBatchResponse(killed, msg) }.getOrElse { sessionManager.getBatchMetadata(batchId).map { metadata => - checkPermission(userName, metadata.username) + fe.getSessionUser(metadata.username) if (OperationState.isTerminal(OperationState.withName(metadata.state))) { new CloseBatchResponse(false, s"The batch[$metadata] has been terminated.") } else if (batchV2Enabled(metadata.requestConf) && metadata.state == "INITIALIZED" && @@ -485,21 +478,21 @@ private[v1] class BatchesResource extends ApiRequestContext with Logging { } else if (batchV2Enabled(metadata.requestConf) && metadata.kyuubiInstance == null) { // code goes here indicates metadata is outdated, recursively calls itself to refresh // the metadata - closeBatchSession(batchId, hs2ProxyUser) + closeBatchSession(batchId) } else if (metadata.kyuubiInstance != fe.connectionUrl) { info(s"Redirecting delete batch[$batchId] to ${metadata.kyuubiInstance}") val internalRestClient = getInternalRestClient(metadata.kyuubiInstance) try { - internalRestClient.deleteBatch(userName, batchId) + internalRestClient.deleteBatch(metadata.username, batchId) } catch { case e: KyuubiRestException => error(s"Error redirecting delete batch[$batchId] to ${metadata.kyuubiInstance}", e) - val (killed, msg) = forceKill(metadata.appMgrInfo, batchId, userName) + val (killed, msg) = forceKill(metadata.appMgrInfo, batchId, metadata.username) new CloseBatchResponse(killed, if (killed) msg else Utils.stringifyException(e)) } } else { // should not happen, but handle this for safe warn(s"Something wrong on deleting batch[$batchId], try forcibly killing application") - val (killed, msg) = forceKill(metadata.appMgrInfo, batchId, userName) + val (killed, msg) = forceKill(metadata.appMgrInfo, batchId, metadata.username) new CloseBatchResponse(killed, msg) } }.getOrElse { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/InternalRestClient.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/InternalRestClient.scala index 8b8a6151303..59d14dacd1e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/InternalRestClient.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/InternalRestClient.scala @@ -33,10 +33,16 @@ import org.apache.kyuubi.service.authentication.InternalSecurityAccessor * @param socketTimeout the socket timeout for http client. * @param connectTimeout the connect timeout for http client. */ -class InternalRestClient(kyuubiInstance: String, socketTimeout: Int, connectTimeout: Int) { - require( - InternalSecurityAccessor.get() != null, - "Internal secure access across Kyuubi instances is not enabled") +class InternalRestClient( + kyuubiInstance: String, + socketTimeout: Int, + connectTimeout: Int, + securityEnabled: Boolean) { + if (securityEnabled) { + require( + InternalSecurityAccessor.get() != null, + "Internal secure access across Kyuubi instances is not enabled") + } private val internalBatchRestApi = new BatchRestApi(initKyuubiRestClient()) @@ -54,17 +60,19 @@ class InternalRestClient(kyuubiInstance: String, socketTimeout: Int, connectTime def deleteBatch(user: String, batchId: String): CloseBatchResponse = { withAuthUser(user) { - internalBatchRestApi.deleteBatch(batchId, null) + internalBatchRestApi.deleteBatch(batchId) } } private def initKyuubiRestClient(): KyuubiRestClient = { - KyuubiRestClient.builder(s"http://$kyuubiInstance") + val builder = KyuubiRestClient.builder(s"http://$kyuubiInstance") .apiVersion(KyuubiRestClient.ApiVersion.V1) .socketTimeout(socketTimeout) .connectionTimeout(connectTimeout) - .authHeaderGenerator(InternalRestClient.internalAuthHeaderGenerator) - .build() + if (securityEnabled) { + builder.authHeaderGenerator(InternalRestClient.internalAuthHeaderGenerator) + } + builder.build() } private def withAuthUser[T](user: String)(f: => T): T = { @@ -82,7 +90,7 @@ object InternalRestClient { override def initialValue(): String = null } - final val internalAuthHeaderGenerator = new AuthHeaderGenerator { + final lazy val internalAuthHeaderGenerator = new AuthHeaderGenerator { override def generateAuthHeader(): String = { val authUser = AUTH_USER.get() require(authUser != null, "The auth user shall be not null") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/OperationsResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/OperationsResource.scala index fdde5bbc5b2..e7a15ab9293 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/OperationsResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/OperationsResource.scala @@ -26,13 +26,12 @@ import scala.util.control.NonFatal import io.swagger.v3.oas.annotations.media.{Content, Schema} import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag -import org.apache.hive.service.rpc.thrift._ import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.client.api.v1.dto._ -import org.apache.kyuubi.events.KyuubiOperationEvent import org.apache.kyuubi.operation.{FetchOrientation, KyuubiOperation, OperationHandle} import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ @Tag(name = "Operation") @Produces(Array(MediaType.APPLICATION_JSON)) @@ -54,7 +53,7 @@ private[v1] class OperationsResource extends ApiRequestContext with Logging { try { val opHandle = OperationHandle(operationHandleStr) val operation = fe.be.sessionManager.operationManager.getOperation(opHandle) - KyuubiOperationEvent(operation.asInstanceOf[KyuubiOperation]) + ApiUtils.operationEvent(operation.asInstanceOf[KyuubiOperation]) } catch { case NonFatal(e) => val errorMsg = "Error getting an operation event" diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala index 10a55786798..928bb207a1e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala @@ -28,7 +28,6 @@ import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema} import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import org.apache.commons.lang3.StringUtils -import org.apache.hive.service.rpc.thrift.{TGetInfoType, TProtocolVersion} import org.apache.kyuubi.Logging import org.apache.kyuubi.client.api.v1.dto @@ -37,6 +36,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle} import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils} import org.apache.kyuubi.session.{KyuubiSession, SessionHandle} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TGetInfoType, TProtocolVersion} @Tag(name = "Session") @Produces(Array(MediaType.APPLICATION_JSON)) @@ -69,26 +69,7 @@ private[v1] class SessionsResource extends ApiRequestContext with Logging { @Path("{sessionHandle}") def sessionInfo(@PathParam("sessionHandle") sessionHandleStr: String): dto.KyuubiSessionEvent = { try { - sessionManager.getSession(sessionHandleStr) - .asInstanceOf[KyuubiSession].getSessionEvent.map(event => - dto.KyuubiSessionEvent.builder - .sessionId(event.sessionId) - .clientVersion(event.clientVersion) - .sessionType(event.sessionType) - .sessionName(event.sessionName) - .user(event.user) - .clientIp(event.clientIP) - .serverIp(event.serverIP) - .conf(event.conf.asJava) - .remoteSessionId(event.remoteSessionId) - .engineId(event.engineId) - .eventTime(event.eventTime) - .openedTime(event.openedTime) - .startTime(event.startTime) - .endTime(event.endTime) - .totalOperations(event.totalOperations) - .exception(event.exception.orNull) - .build).get + ApiUtils.sessionEvent(sessionManager.getSession(sessionHandleStr).asInstanceOf[KyuubiSession]) } catch { case NonFatal(e) => val errorMsg = s"Invalid $sessionHandleStr" diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala index bb9f1553d39..eb8fb2caa69 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala @@ -27,17 +27,17 @@ import javax.ws.rs.core.NewCookie import scala.collection.mutable import org.apache.hadoop.hive.shims.Utils -import org.apache.thrift.TProcessor -import org.apache.thrift.protocol.TProtocolFactory -import org.apache.thrift.server.TServlet import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER import org.apache.kyuubi.server.http.authentication.AuthenticationFilter -import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER import org.apache.kyuubi.server.http.util.{CookieSigner, HttpAuthUtils, SessionManager} +import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory +import org.apache.kyuubi.shaded.thrift.TProcessor +import org.apache.kyuubi.shaded.thrift.protocol.TProtocolFactory +import org.apache.kyuubi.shaded.thrift.server.TServlet class ThriftHttpServlet( processor: TProcessor, @@ -136,7 +136,7 @@ class ThriftHttpServlet( } else SessionManager.setForwardedAddresses(List.empty[String]) // Generate new cookie and add it to the response - if (requireNewCookie && !authFactory.isNoSaslEnabled) { + if (requireNewCookie && !authFactory.noSaslEnabled) { val cookieToken = HttpAuthUtils.createCookieToken(clientUserName) val hs2Cookie = createCookie(signer.signCookie(cookieToken)) if (isHttpOnlyCookie) response.setHeader("SET-COOKIE", getHttpOnlyCookieHeader(hs2Cookie)) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala index 523d2490753..15b387607ea 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala @@ -27,12 +27,12 @@ import scala.collection.mutable import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.{AUTHENTICATION_METHOD, FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER} +import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER import org.apache.kyuubi.service.authentication.{AuthTypes, InternalSecurityAccessor} import org.apache.kyuubi.service.authentication.AuthTypes.{KERBEROS, NOSASL} class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging { import AuthenticationFilter._ - import AuthenticationHandler._ import AuthSchemes._ private[authentication] val authSchemeHandlers = diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationHandler.scala index bf2cb5bbecb..a0b3fb4ab37 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationHandler.scala @@ -20,13 +20,11 @@ package org.apache.kyuubi.server.http.authentication import javax.security.sasl.AuthenticationException import javax.servlet.http.{HttpServletRequest, HttpServletResponse} -import org.apache.hadoop.security.authentication.server.HttpConstants - import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme +import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER trait AuthenticationHandler { - import AuthenticationHandler._ /** * HTTP header prefix used during the authentication sequence. @@ -103,23 +101,10 @@ trait AuthenticationHandler { authorization = authorization.stripPrefix(":").trim } // Authorization header must have a payload - if (authorization == null || authorization.isEmpty()) { + if (authorization == null || authorization.isEmpty) { throw new AuthenticationException( "Authorization header received from the client does not contain any data.") } authorization } } - -object AuthenticationHandler { - - /** - * HTTP header used by the SPNEGO server endpoint during an authentication sequence. - */ - final val WWW_AUTHENTICATE: String = HttpConstants.WWW_AUTHENTICATE_HEADER - - /** - * HTTP header used by the client endpoint during an authentication sequence. - */ - final val AUTHORIZATION_HEADER = "Authorization" -} diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala index 57ce2e60e8f..76560cabb55 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala @@ -24,12 +24,12 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme +import org.apache.kyuubi.server.http.util.HttpAuthUtils.{AUTHORIZATION_HEADER, WWW_AUTHENTICATE_HEADER} import org.apache.kyuubi.service.authentication.{AuthenticationProviderFactory, AuthMethods} import org.apache.kyuubi.service.authentication.AuthTypes._ class BasicAuthenticationHandler(basicAuthType: AuthType) extends AuthenticationHandler with Logging { - import AuthenticationHandler._ private var conf: KyuubiConf = _ private val allowAnonymous = basicAuthType == NOSASL || basicAuthType == NONE @@ -75,7 +75,7 @@ class BasicAuthenticationHandler(basicAuthType: AuthType) authUser = creds.take(1).headOption.filterNot(_.isEmpty).getOrElse("anonymous") } else { if (creds.size < 2 || creds(0).trim.isEmpty || creds(1).trim.isEmpty) { - response.setHeader(WWW_AUTHENTICATE, authScheme.toString) + response.setHeader(WWW_AUTHENTICATE_HEADER, authScheme.toString) response.setStatus(HttpServletResponse.SC_UNAUTHORIZED) } else { val Seq(user, password) = creds.toSeq.take(2) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KerberosAuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KerberosAuthenticationHandler.scala index 04603f30a41..7220e3906eb 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KerberosAuthenticationHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KerberosAuthenticationHandler.scala @@ -27,15 +27,15 @@ import javax.servlet.ServletException import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.apache.hadoop.security.authentication.util.KerberosName +import org.apache.hadoop.security.authentication.util.KerberosUtil._ import org.ietf.jgss.{GSSContext, GSSCredential, GSSManager, Oid} import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme +import org.apache.kyuubi.server.http.util.HttpAuthUtils.{NEGOTIATE, WWW_AUTHENTICATE_HEADER} class KerberosAuthenticationHandler extends AuthenticationHandler with Logging { - import AuthenticationHandler._ - import AuthSchemes._ - import KerberosUtil._ private var gssManager: GSSManager = _ private var conf: KyuubiConf = _ @@ -143,7 +143,7 @@ class KerberosAuthenticationHandler extends AuthenticationHandler with Logging { val serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length) if (serverToken != null && serverToken.nonEmpty) { val authenticate = Base64.getEncoder.encodeToString(serverToken) - response.setHeader(WWW_AUTHENTICATE, s"$NEGOTIATE $authenticate") + response.setHeader(WWW_AUTHENTICATE_HEADER, s"$NEGOTIATE $authenticate") } if (!gssContext.isEstablished) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiInternalAuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiInternalAuthenticationHandler.scala index 7af6389ccee..d910f4a8396 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiInternalAuthenticationHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiInternalAuthenticationHandler.scala @@ -17,17 +17,17 @@ package org.apache.kyuubi.server.http.authentication -import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.util.Base64 import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme +import org.apache.kyuubi.server.http.util.HttpAuthUtils.WWW_AUTHENTICATE_HEADER import org.apache.kyuubi.service.authentication.InternalSecurityAccessor class KyuubiInternalAuthenticationHandler extends AuthenticationHandler with Logging { - import AuthenticationHandler._ private var conf: KyuubiConf = _ override val authScheme: AuthScheme = AuthSchemes.KYUUBI_INTERNAL @@ -48,10 +48,10 @@ class KyuubiInternalAuthenticationHandler extends AuthenticationHandler with Log val authorization = getAuthorization(request) val inputToken = Option(authorization).map(a => Base64.getDecoder.decode(a.getBytes())) .getOrElse(Array.empty[Byte]) - val creds = new String(inputToken, Charset.forName("UTF-8")).split(":") + val creds = new String(inputToken, StandardCharsets.UTF_8).split(":") if (creds.size < 2 || creds(0).trim.isEmpty || creds(1).trim.isEmpty) { - response.setHeader(WWW_AUTHENTICATE, authScheme.toString) + response.setHeader(WWW_AUTHENTICATE_HEADER, authScheme.toString) response.setStatus(HttpServletResponse.SC_UNAUTHORIZED) } else { val Seq(user, password) = creds.toSeq.take(2) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala index 7bb11747668..e840a307c47 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala @@ -17,19 +17,33 @@ package org.apache.kyuubi.server.http.util +import java.nio.charset.StandardCharsets import java.security.SecureRandom import java.util -import java.util.StringTokenizer +import java.util.{Base64, StringTokenizer} import scala.collection.mutable import org.apache.kyuubi.Logging object HttpAuthUtils extends Logging { - val WWW_AUTHENTICATE = "WWW-Authenticate" - val AUTHORIZATION = "Authorization" - val BASIC = "Basic" + // HTTP header used by the server endpoint during an authentication sequence. + val WWW_AUTHENTICATE_HEADER = "WWW-Authenticate" + // HTTP header used by the client endpoint during an authentication sequence. + val AUTHORIZATION_HEADER = "Authorization" + // HTTP header prefix used by the SPNEGO client/server endpoints during an + // authentication sequence. val NEGOTIATE = "Negotiate" + // HTTP header prefix used during the Basic authentication sequence. + val BASIC = "Basic" + // HTTP header prefix used during the Basic authentication sequence. + val DIGEST = "Digest" + + // RFC 7617: The 'Basic' HTTP Authentication Scheme + def basicAuthorizationHeader(userId: String, password: String = "none"): String = + "BASIC " + new String( + Base64.getEncoder.encode(s"$userId:$password".getBytes()), + StandardCharsets.UTF_8) private val COOKIE_ATTR_SEPARATOR = "&" private val COOKIE_CLIENT_USER_NAME = "cu" diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/MetadataManager.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/MetadataManager.scala index 1da9e1f3148..6dd0e76e0b3 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/MetadataManager.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/MetadataManager.scala @@ -32,6 +32,7 @@ import org.apache.kyuubi.server.metadata.api.{Metadata, MetadataFilter} import org.apache.kyuubi.service.AbstractService import org.apache.kyuubi.session.SessionType import org.apache.kyuubi.util.{ClassUtils, JdbcUtils, ThreadUtils} +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay class MetadataManager extends AbstractService("MetadataManager") { import MetadataManager._ @@ -209,7 +210,8 @@ class MetadataManager extends AbstractService("MetadataManager") { } } - metadataCleaner.scheduleWithFixedDelay( + scheduleTolerableRunnableWithFixedDelay( + metadataCleaner, cleanerTask, interval, interval, @@ -298,7 +300,9 @@ class MetadataManager extends AbstractService("MetadataManager") { } } } - requestsAsyncRetryTrigger.scheduleWithFixedDelay( + + scheduleTolerableRunnableWithFixedDelay( + requestsAsyncRetryTrigger, triggerTask, requestsRetryInterval, requestsRetryInterval, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStore.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStore.scala index 9b1c89d779b..0a6d402296b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStore.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStore.scala @@ -74,7 +74,7 @@ class JDBCMetadataStore(conf: KyuubiConf) extends MetadataStore with Logging { JDBCMetadataStoreConf.getMetadataStoreJDBCDataSourceProperties(conf) private val hikariConfig = new HikariConfig(datasourceProperties) hikariConfig.setDriverClassName(driverClass) - hikariConfig.setJdbcUrl(conf.get(METADATA_STORE_JDBC_URL)) + hikariConfig.setJdbcUrl(getMetadataStoreJdbcUrl(conf)) hikariConfig.setUsername(conf.get(METADATA_STORE_JDBC_USER)) hikariConfig.setPassword(conf.get(METADATA_STORE_JDBC_PASSWORD)) hikariConfig.setPoolName("jdbc-metadata-store-pool") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStoreConf.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStoreConf.scala index 96a5539fb27..e2b06541ddc 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStoreConf.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/metadata/jdbc/JDBCMetadataStoreConf.scala @@ -19,12 +19,26 @@ package org.apache.kyuubi.server.metadata.jdbc import java.util.Properties +import org.apache.kyuubi.Utils import org.apache.kyuubi.config.{ConfigEntry, KyuubiConf, OptionalConfigEntry} import org.apache.kyuubi.config.KyuubiConf.buildConf object JDBCMetadataStoreConf { final val METADATA_STORE_JDBC_DATASOURCE_PREFIX = "kyuubi.metadata.store.jdbc.datasource" + def getMetadataStoreJdbcUrl(conf: KyuubiConf): String = { + val rawJdbcUrl = conf.get(METADATA_STORE_JDBC_URL) + if (rawJdbcUrl.contains("")) { + rawJdbcUrl.replace( + "", + sys.env.getOrElse( + "KYUUBI_HOME", + Utils.getCodeSourceLocation(getClass).split("kyuubi-server").head)) + } else { + rawJdbcUrl + } + } + /** Get metadata store jdbc datasource properties. */ def getMetadataStoreJDBCDataSourceProperties(conf: KyuubiConf): Properties = { val datasourceProperties = new Properties() @@ -70,14 +84,14 @@ object JDBCMetadataStoreConf { val METADATA_STORE_JDBC_URL: ConfigEntry[String] = buildConf("kyuubi.metadata.store.jdbc.url") - .doc("The JDBC url for server JDBC metadata store. By default, it is a SQLite" + - " database url, and the state information is not shared across kyuubi instances. To" + - " enable high availability for multiple kyuubi instances," + - " please specify a production JDBC url.") + .doc("The JDBC url for server JDBC metadata store. By default, it is a SQLite database " + + "url, and the state information is not shared across Kyuubi instances. To enable high " + + "availability for multiple kyuubi instances, please specify a production JDBC url. " + + "Note: this value support the variables substitution: ``.") .version("1.6.0") .serverOnly .stringConf - .createWithDefault("jdbc:sqlite:kyuubi_state_store.db") + .createWithDefault("jdbc:sqlite:/kyuubi_state_store.db") val METADATA_STORE_JDBC_USER: ConfigEntry[String] = buildConf("kyuubi.metadata.store.jdbc.user") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLCommandHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLCommandHandler.scala index 5f7a07f5875..ad91335bf9c 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLCommandHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLCommandHandler.scala @@ -25,7 +25,6 @@ import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} import scala.util.{Failure, Success} import io.netty.channel.{ChannelHandlerContext, SimpleChannelInboundHandler} -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.{KyuubiSQLException, Logging} import org.apache.kyuubi.config.KyuubiReservedKeys._ @@ -35,6 +34,7 @@ import org.apache.kyuubi.server.mysql.MySQLCommandHandler._ import org.apache.kyuubi.server.mysql.constant.MySQLCtxAttrKey._ import org.apache.kyuubi.service.BackendService import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion object MySQLCommandHandler { val connIdCounter = new AtomicInteger diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLQueryResult.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLQueryResult.scala index 59371b923e9..b2bba52ca2f 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLQueryResult.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/MySQLQueryResult.scala @@ -19,9 +19,8 @@ package org.apache.kyuubi.server.mysql import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.server.mysql.constant.MySQLDataType +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ object MySQLQueryResult { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/constant/MySQLDataType.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/constant/MySQLDataType.scala index a3b21fad84d..a1818bb486b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/constant/MySQLDataType.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/mysql/constant/MySQLDataType.scala @@ -19,7 +19,7 @@ package org.apache.kyuubi.server.mysql.constant import java.sql.Types -import org.apache.hive.service.rpc.thrift.TTypeId +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId sealed abstract class MySQLDataType(val value: Int) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/Query.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/Query.scala index dc9de4ae2e0..83c4d8281fb 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/Query.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/Query.scala @@ -30,7 +30,6 @@ import scala.collection.mutable import Slug.Context.{EXECUTING_QUERY, QUEUED_QUERY} import com.google.common.hash.Hashing import io.trino.client.QueryResults -import org.apache.hive.service.rpc.thrift.{TBoolValue, TColumnDesc, TColumnValue, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TProtocolVersion, TRow, TRowSet, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle, OperationState, OperationStatus} import org.apache.kyuubi.operation.OperationState.{FINISHED, INITIALIZED, OperationState, PENDING} @@ -38,6 +37,7 @@ import org.apache.kyuubi.server.trino.api.Query.KYUUBI_SESSION_ID import org.apache.kyuubi.service.BackendService import org.apache.kyuubi.service.TFrontendService.OK_STATUS import org.apache.kyuubi.session.SessionHandle +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TBoolValue, TColumnDesc, TColumnValue, TGetResultSetMetadataResp, TPrimitiveTypeEntry, TProtocolVersion, TRow, TRowSet, TTableSchema, TTypeDesc, TTypeEntry, TTypeId} case class Query( queryId: QueryId, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/TrinoContext.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/TrinoContext.scala index 842f0ceec73..fd364321d0d 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/TrinoContext.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/trino/api/TrinoContext.scala @@ -28,11 +28,11 @@ import scala.collection.JavaConverters._ import com.google.common.collect.ImmutableList import io.trino.client.{ClientStandardTypes, ClientTypeSignature, ClientTypeSignatureParameter, Column, NamedClientTypeSignature, QueryError, QueryResults, RowFieldName, StatementStats, Warning} import io.trino.client.ProtocolHeaders.TRINO_HEADERS -import org.apache.hive.service.rpc.thrift.{TCLIServiceConstants, TGetResultSetMetadataResp, TRowSet, TTypeEntry, TTypeId} import org.apache.kyuubi.operation.OperationState.FINISHED import org.apache.kyuubi.operation.OperationStatus import org.apache.kyuubi.server.trino.api.Query.KYUUBI_SESSION_ID +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TCLIServiceConstants, TGetResultSetMetadataResp, TRowSet, TTypeEntry, TTypeId} // TODO: Support replace `preparedStatement` for Trino-jdbc /** diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiBatchSession.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiBatchSession.scala index 8489e6d307b..531bbc3af87 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiBatchSession.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiBatchSession.scala @@ -19,8 +19,6 @@ package org.apache.kyuubi.session import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.client.util.BatchUtils._ import org.apache.kyuubi.config.{KyuubiConf, KyuubiReservedKeys} import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_BATCH_PRIORITY @@ -30,6 +28,7 @@ import org.apache.kyuubi.events.{EventBus, KyuubiSessionEvent} import org.apache.kyuubi.operation.OperationState import org.apache.kyuubi.server.metadata.api.Metadata import org.apache.kyuubi.session.SessionType.SessionType +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class KyuubiBatchSession( user: String, @@ -81,12 +80,12 @@ class KyuubiBatchSession( sessionConf.getBatchConf(batchType) ++ sessionManager.validateBatchConf(conf) val optimizedConf: Map[String, String] = { - val confOverlay = sessionManager.sessionConfAdvisor.getConfOverlay( + val confOverlay = sessionManager.sessionConfAdvisor.map(_.getConfOverlay( user, - normalizedConf.asJava) + normalizedConf.asJava).asScala).reduce(_ ++ _) if (confOverlay != null) { val overlayConf = new KyuubiConf(false) - confOverlay.asScala.foreach { case (k, v) => overlayConf.set(k, v) } + confOverlay.foreach { case (k, v) => overlayConf.set(k, v) } normalizedConf ++ overlayConf.getBatchConf(batchType) } else { warn(s"the server plugin return null value for user: $user, ignore it") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSession.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSession.scala index a4c345af39c..19f4039876b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSession.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSession.scala @@ -17,13 +17,13 @@ package org.apache.kyuubi.session import com.codahale.metrics.MetricRegistry -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_SESSION_CONNECTION_URL_KEY, KYUUBI_SESSION_REAL_USER_KEY} import org.apache.kyuubi.events.{EventBus, KyuubiSessionEvent} import org.apache.kyuubi.metrics.MetricsConstants.{CONN_OPEN, CONN_TOTAL} import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.session.SessionType.SessionType +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion abstract class KyuubiSession( protocol: TProtocolVersion, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionImpl.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionImpl.scala index 6dd1810a8de..a5d160e0714 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionImpl.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionImpl.scala @@ -21,12 +21,11 @@ import java.util.Base64 import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ - import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.client.KyuubiSyncThriftClient import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.config.KyuubiConf.EngineOpenOnFailure._ import org.apache.kyuubi.config.KyuubiReservedKeys.{KYUUBI_ENGINE_CREDENTIALS_KEY, KYUUBI_SESSION_HANDLE_KEY, KYUUBI_SESSION_SIGN_PUBLICKEY, KYUUBI_SESSION_USER_SIGN} import org.apache.kyuubi.engine.{EngineRef, KyuubiApplicationManager} import org.apache.kyuubi.events.{EventBus, KyuubiSessionEvent} @@ -35,6 +34,8 @@ import org.apache.kyuubi.operation.{Operation, OperationHandle} import org.apache.kyuubi.operation.log.OperationLog import org.apache.kyuubi.service.authentication.InternalSecurityAccessor import org.apache.kyuubi.session.SessionType.SessionType +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.thrift.transport.TTransportException import org.apache.kyuubi.sql.parser.server.KyuubiParser import org.apache.kyuubi.sql.plan.command.RunnableCommand import org.apache.kyuubi.util.SignUtils @@ -53,11 +54,11 @@ class KyuubiSessionImpl( override val sessionType: SessionType = SessionType.INTERACTIVE private[kyuubi] val optimizedConf: Map[String, String] = { - val confOverlay = sessionManager.sessionConfAdvisor.getConfOverlay( + val confOverlay = sessionManager.sessionConfAdvisor.map(_.getConfOverlay( user, - normalizedConf.asJava) + normalizedConf.asJava).asScala).reduce(_ ++ _) if (confOverlay != null) { - normalizedConf ++ confOverlay.asScala + normalizedConf ++ confOverlay } else { warn(s"the server plugin return null value for user: $user, ignore it") normalizedConf @@ -99,12 +100,12 @@ class KyuubiSessionImpl( sessionManager.getConf) } - private var _client: KyuubiSyncThriftClient = _ + @volatile private var _client: KyuubiSyncThriftClient = _ def client: KyuubiSyncThriftClient = _client - private var _engineSessionHandle: SessionHandle = _ + @volatile private var _engineSessionHandle: SessionHandle = _ - private var openSessionError: Option[Throwable] = None + @volatile private var openSessionError: Option[Throwable] = None override def open(): Unit = handleSessionException { traceMetricsOnOpen() @@ -141,10 +142,21 @@ class KyuubiSessionImpl( val maxAttempts = sessionManager.getConf.get(ENGINE_OPEN_MAX_ATTEMPTS) val retryWait = sessionManager.getConf.get(ENGINE_OPEN_RETRY_WAIT) + val openOnFailure = + EngineOpenOnFailure.withName(sessionManager.getConf.get(ENGINE_OPEN_ON_FAILURE)) var attempt = 0 var shouldRetry = true while (attempt <= maxAttempts && shouldRetry) { val (host, port) = engine.getOrCreate(discoveryClient, extraEngineLog) + + def deregisterEngine(): Unit = + try { + engine.deregister(discoveryClient, (host, port)) + } catch { + case e: Throwable => + warn(s"Error on de-registering engine [${engine.engineSpace} $host:$port]", e) + } + try { val passwd = if (sessionManager.getConf.get(ENGINE_SECURITY_ENABLED)) { @@ -159,7 +171,7 @@ class KyuubiSessionImpl( s" with ${_engineSessionHandle}]") shouldRetry = false } catch { - case e: org.apache.thrift.transport.TTransportException + case e: TTransportException if attempt < maxAttempts && e.getCause.isInstanceOf[java.net.ConnectException] && e.getCause.getMessage.contains("Connection refused") => warn( @@ -167,6 +179,10 @@ class KyuubiSessionImpl( s" $attempt/$maxAttempts times, retrying", e.getCause) Thread.sleep(retryWait) + openOnFailure match { + case DEREGISTER_IMMEDIATELY => deregisterEngine() + case _ => + } shouldRetry = true case e: Throwable => error( @@ -174,6 +190,10 @@ class KyuubiSessionImpl( s" for $user session failed", e) openSessionError = Some(e) + openOnFailure match { + case DEREGISTER_IMMEDIATELY | DEREGISTER_AFTER_RETRY => deregisterEngine() + case _ => + } throw e } finally { attempt += 1 @@ -290,7 +310,7 @@ class KyuubiSessionImpl( private val engineAliveTimeout = sessionConf.get(KyuubiConf.ENGINE_ALIVE_TIMEOUT) private val aliveProbeEnabled = sessionConf.get(KyuubiConf.ENGINE_ALIVE_PROBE_ENABLED) private val engineAliveMaxFailCount = sessionConf.get(KyuubiConf.ENGINE_ALIVE_MAX_FAILURES) - private var engineAliveFailCount = 0 + @volatile private var engineAliveFailCount = 0 def checkEngineConnectionAlive(): Boolean = { try { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala index 02a3ee32c7c..0696af74fa4 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala @@ -23,7 +23,6 @@ import scala.collection.JavaConverters._ import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.KyuubiSQLException import org.apache.kyuubi.client.api.v1.dto.{Batch, BatchRequest} @@ -39,8 +38,10 @@ import org.apache.kyuubi.operation.{KyuubiOperationManager, OperationState} import org.apache.kyuubi.plugin.{GroupProvider, PluginLoader, SessionConfAdvisor} import org.apache.kyuubi.server.metadata.{MetadataManager, MetadataRequestsRetryRef} import org.apache.kyuubi.server.metadata.api.{Metadata, MetadataFilter} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion import org.apache.kyuubi.sql.parser.server.KyuubiParser import org.apache.kyuubi.util.{SignUtils, ThreadUtils} +import org.apache.kyuubi.util.ThreadUtils.scheduleTolerableRunnableWithFixedDelay class KyuubiSessionManager private (name: String) extends SessionManager(name) { @@ -58,7 +59,7 @@ class KyuubiSessionManager private (name: String) extends SessionManager(name) { if (conf.isRESTEnabled) Some(new MetadataManager()) else None // lazy is required for plugins since the conf is null when this class initialization - lazy val sessionConfAdvisor: SessionConfAdvisor = PluginLoader.loadSessionConfAdvisor(conf) + lazy val sessionConfAdvisor: Seq[SessionConfAdvisor] = PluginLoader.loadSessionConfAdvisor(conf) lazy val groupProvider: GroupProvider = PluginLoader.loadGroupProvider(conf) private var limiter: Option[SessionLimiter] = None @@ -396,20 +397,22 @@ class KyuubiSessionManager private (name: String) extends SessionManager(name) { private def startEngineAliveChecker(): Unit = { val interval = conf.get(KyuubiConf.ENGINE_ALIVE_PROBE_INTERVAL) val checkTask: Runnable = () => { - allSessions().foreach { session => - if (!session.asInstanceOf[KyuubiSessionImpl].checkEngineConnectionAlive()) { + allSessions().foreach { + case session: KyuubiSessionImpl => try { - closeSession(session.handle) - logger.info(s"The session ${session.handle} has been closed " + - s"due to engine unresponsiveness (checked by the engine alive checker).") + if (!session.checkEngineConnectionAlive()) { + closeSession(session.handle) + logger.info(s"The session ${session.handle} has been closed " + + s"due to engine unresponsiveness (checked by the engine alive checker).") + } } catch { - case e: KyuubiSQLException => - warn(s"Error closing session ${session.handle}", e) + case e: Throwable => warn(s"Error closing session ${session.handle}", e) } - } + case _ => } } - engineConnectionAliveChecker.scheduleWithFixedDelay( + scheduleTolerableRunnableWithFixedDelay( + engineConnectionAliveChecker, checkTask, interval, interval, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/DescribeSession.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/DescribeSession.scala index 934aac9a2f0..e1d77f296e7 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/DescribeSession.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/DescribeSession.scala @@ -19,10 +19,9 @@ package org.apache.kyuubi.sql.plan.command import scala.collection.mutable.ListBuffer -import org.apache.hive.service.rpc.thrift.TTypeId - import org.apache.kyuubi.operation.IterableFetchIterator import org.apache.kyuubi.session.KyuubiSession +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId import org.apache.kyuubi.sql.schema.{Column, Row, Schema} /** diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/RunnableCommand.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/RunnableCommand.scala index deda7d0061f..cdfb515bd3a 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/RunnableCommand.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/plan/command/RunnableCommand.scala @@ -17,13 +17,12 @@ package org.apache.kyuubi.sql.plan.command -import org.apache.hive.service.rpc.thrift.{TProtocolVersion, TRowSet} - import org.apache.kyuubi.operation.FetchIterator import org.apache.kyuubi.operation.FetchOrientation.{FETCH_FIRST, FETCH_NEXT, FETCH_PRIOR, FetchOrientation} import org.apache.kyuubi.session.KyuubiSession +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TProtocolVersion, TRowSet} import org.apache.kyuubi.sql.plan.KyuubiTreeNode -import org.apache.kyuubi.sql.schema.{Row, RowSetHelper, Schema} +import org.apache.kyuubi.sql.schema.{Row, Schema, ServerTRowSetGenerator} trait RunnableCommand extends KyuubiTreeNode { @@ -45,7 +44,7 @@ trait RunnableCommand extends KyuubiTreeNode { case FETCH_FIRST => iter.fetchAbsolute(0) } val taken = iter.take(rowSetSize) - val resultRowSet = RowSetHelper.toTRowSet( + val resultRowSet = new ServerTRowSetGenerator().toTRowSet( taken.toList, resultSchema, protocolVersion) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/Column.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/Column.scala index 5b71ffd44cf..0b27f5ce500 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/Column.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/Column.scala @@ -17,6 +17,6 @@ package org.apache.kyuubi.sql.schema -import org.apache.hive.service.rpc.thrift.TTypeId +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId case class Column(name: String, dataType: TTypeId, comment: Option[String] = None) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/RowSetHelper.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/RowSetHelper.scala deleted file mode 100644 index d76efaa4b8b..00000000000 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/RowSetHelper.scala +++ /dev/null @@ -1,210 +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. - */ - -package org.apache.kyuubi.sql.schema - -import java.util - -import scala.collection.JavaConverters._ - -import org.apache.hive.service.rpc.thrift._ - -import org.apache.kyuubi.util.RowSetUtils._ - -object RowSetHelper { - - def toTRowSet( - rows: Seq[Row], - schema: Schema, - protocolVersion: TProtocolVersion): TRowSet = { - if (protocolVersion.getValue < TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V6.getValue) { - toRowBasedSet(rows, schema) - } else { - toColumnBasedSet(rows, schema) - } - } - - def toRowBasedSet(rows: Seq[Row], schema: Schema): TRowSet = { - var i = 0 - val rowSize = rows.length - val tRows = new java.util.ArrayList[TRow](rowSize) - while (i < rowSize) { - val row = rows(i) - val tRow = new TRow() - var j = 0 - val columnSize = row.length - while (j < columnSize) { - val columnValue = toTColumnValue(j, row, schema) - tRow.addToColVals(columnValue) - j += 1 - } - i += 1 - tRows.add(tRow) - } - new TRowSet(0, tRows) - } - - private def toTColumnValue( - ordinal: Int, - row: Row, - types: Schema): TColumnValue = { - types(ordinal).dataType match { - case TTypeId.BOOLEAN_TYPE => - val boolValue = new TBoolValue - if (!row.isNullAt(ordinal)) boolValue.setValue(row.getBoolean(ordinal)) - TColumnValue.boolVal(boolValue) - - case TTypeId.BINARY_TYPE => - val byteValue = new TByteValue - if (!row.isNullAt(ordinal)) byteValue.setValue(row.getByte(ordinal)) - TColumnValue.byteVal(byteValue) - - case TTypeId.TINYINT_TYPE => - val tI16Value = new TI16Value - if (!row.isNullAt(ordinal)) tI16Value.setValue(row.getShort(ordinal)) - TColumnValue.i16Val(tI16Value) - - case TTypeId.INT_TYPE => - val tI32Value = new TI32Value - if (!row.isNullAt(ordinal)) tI32Value.setValue(row.getInt(ordinal)) - TColumnValue.i32Val(tI32Value) - - case TTypeId.BIGINT_TYPE => - val tI64Value = new TI64Value - if (!row.isNullAt(ordinal)) tI64Value.setValue(row.getLong(ordinal)) - TColumnValue.i64Val(tI64Value) - - case TTypeId.FLOAT_TYPE => - val tDoubleValue = new TDoubleValue - if (!row.isNullAt(ordinal)) { - val doubleValue = java.lang.Double.valueOf(row.getFloat(ordinal).toString) - tDoubleValue.setValue(doubleValue) - } - TColumnValue.doubleVal(tDoubleValue) - - case TTypeId.DOUBLE_TYPE => - val tDoubleValue = new TDoubleValue - if (!row.isNullAt(ordinal)) tDoubleValue.setValue(row.getDouble(ordinal)) - TColumnValue.doubleVal(tDoubleValue) - - case TTypeId.STRING_TYPE => - val tStringValue = new TStringValue - if (!row.isNullAt(ordinal)) tStringValue.setValue(row.getString(ordinal)) - TColumnValue.stringVal(tStringValue) - - case _ => - val tStrValue = new TStringValue - if (!row.isNullAt(ordinal)) { - tStrValue.setValue((row.get(ordinal), types(ordinal).dataType).toString()) - } - TColumnValue.stringVal(tStrValue) - } - } - - def toColumnBasedSet(rows: Seq[Row], schema: Schema): TRowSet = { - val rowSize = rows.length - val tRowSet = new TRowSet(0, new java.util.ArrayList[TRow](rowSize)) - var i = 0 - val columnSize = schema.length - while (i < columnSize) { - val field = schema(i) - val tColumn = toTColumn(rows, i, field.dataType) - tRowSet.addToColumns(tColumn) - i += 1 - } - tRowSet - } - - private def toTColumn(rows: Seq[Row], ordinal: Int, typ: TTypeId): TColumn = { - val nulls = new java.util.BitSet() - typ match { - case TTypeId.BOOLEAN_TYPE => - val values = getOrSetAsNull[java.lang.Boolean](rows, ordinal, nulls, true) - TColumn.boolVal(new TBoolColumn(values, nulls)) - - case TTypeId.BINARY_TYPE => - val values = getOrSetAsNull[java.lang.Byte](rows, ordinal, nulls, 0.toByte) - TColumn.byteVal(new TByteColumn(values, nulls)) - - case TTypeId.TINYINT_TYPE => - val values = getOrSetAsNull[java.lang.Short](rows, ordinal, nulls, 0.toShort) - TColumn.i16Val(new TI16Column(values, nulls)) - - case TTypeId.INT_TYPE => - val values = getOrSetAsNull[java.lang.Integer](rows, ordinal, nulls, 0) - TColumn.i32Val(new TI32Column(values, nulls)) - - case TTypeId.BIGINT_TYPE => - val values = getOrSetAsNull[java.lang.Long](rows, ordinal, nulls, 0L) - TColumn.i64Val(new TI64Column(values, nulls)) - - case TTypeId.FLOAT_TYPE => - val values = getOrSetAsNull[java.lang.Float](rows, ordinal, nulls, 0.toFloat) - .asScala.map(n => java.lang.Double.valueOf(n.toString)).asJava - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case TTypeId.DOUBLE_TYPE => - val values = getOrSetAsNull[java.lang.Double](rows, ordinal, nulls, 0.toDouble) - TColumn.doubleVal(new TDoubleColumn(values, nulls)) - - case TTypeId.STRING_TYPE => - val values: util.List[String] = getOrSetAsNull[java.lang.String](rows, ordinal, nulls, "") - TColumn.stringVal(new TStringColumn(values, nulls)) - - case _ => - var i = 0 - val rowSize = rows.length - val values = new java.util.ArrayList[String](rowSize) - while (i < rowSize) { - val row = rows(i) - nulls.set(i, row.isNullAt(ordinal)) - val value = - if (row.isNullAt(ordinal)) { - "" - } else { - (row.get(ordinal), typ).toString() - } - values.add(value) - i += 1 - } - TColumn.stringVal(new TStringColumn(values, nulls)) - } - } - - private def getOrSetAsNull[T]( - rows: Seq[Row], - ordinal: Int, - nulls: java.util.BitSet, - defaultVal: T): java.util.List[T] = { - val size = rows.length - val ret = new java.util.ArrayList[T](size) - var idx = 0 - while (idx < size) { - val row = rows(idx) - val isNull = row.isNullAt(ordinal) - if (isNull) { - nulls.set(idx, true) - ret.add(idx, defaultVal) - } else { - ret.add(idx, row.getAs[T](ordinal)) - } - idx += 1 - } - ret - } - -} diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/SchemaHelper.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/SchemaHelper.scala index f9871ea9fb1..9ff356ccd6c 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/SchemaHelper.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/SchemaHelper.scala @@ -17,7 +17,7 @@ package org.apache.kyuubi.sql.schema -import org.apache.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ object SchemaHelper { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/ServerTRowSetGenerator.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/ServerTRowSetGenerator.scala new file mode 100644 index 00000000000..96294c6eb24 --- /dev/null +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/sql/schema/ServerTRowSetGenerator.scala @@ -0,0 +1,68 @@ +/* + * 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.kyuubi.sql.schema + +import org.apache.kyuubi.engine.result.TRowSetGenerator +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TTypeId._ + +class ServerTRowSetGenerator + extends TRowSetGenerator[Schema, Row, TTypeId] { + + override def getColumnSizeFromSchemaType(schema: Schema): Int = schema.length + + override def getColumnType(schema: Schema, ordinal: Int): TTypeId = schema(ordinal).dataType + + override def isColumnNullAt(row: Row, ordinal: Int): Boolean = row.isNullAt(ordinal) + + override def getColumnAs[T](row: Row, ordinal: Int): T = row.getAs[T](ordinal) + + override def toTColumn(rows: Seq[Row], ordinal: Int, typ: TTypeId): TColumn = { + typ match { + case BOOLEAN_TYPE => asBooleanTColumn(rows, ordinal) + case BINARY_TYPE => asShortTColumn(rows, ordinal) + case TINYINT_TYPE => asShortTColumn(rows, ordinal) + case INT_TYPE => asIntegerTColumn(rows, ordinal) + case BIGINT_TYPE => asLongTColumn(rows, ordinal) + case FLOAT_TYPE => asFloatTColumn(rows, ordinal) + case DOUBLE_TYPE => asDoubleTColumn(rows, ordinal) + case STRING_TYPE => asStringTColumn(rows, ordinal) + case _ => + asStringTColumn( + rows, + ordinal, + convertFunc = (row, ordinal) => (row.get(ordinal), typ).toString()) + } + } + + override def toTColumnValue(row: Row, ordinal: Int, types: Schema): TColumnValue = { + getColumnType(types, ordinal) match { + case BOOLEAN_TYPE => asBooleanTColumnValue(row, ordinal) + case BINARY_TYPE => asByteTColumnValue(row, ordinal) + case TINYINT_TYPE => asShortTColumnValue(row, ordinal) + case INT_TYPE => asIntegerTColumnValue(row, ordinal) + case BIGINT_TYPE => asLongTColumnValue(row, ordinal) + case FLOAT_TYPE => asFloatTColumnValue(row, ordinal) + case DOUBLE_TYPE => asDoubleTColumnValue(row, ordinal) + case STRING_TYPE => asStringTColumnValue(row, ordinal) + case otherType => + asStringTColumnValue(row, ordinal, rawValue => (rawValue, otherType).toString()) + } + } + +} diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/util/KubernetesUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/util/KubernetesUtils.scala index 9da3408a336..02b52f9266e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/util/KubernetesUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/util/KubernetesUtils.scala @@ -120,8 +120,8 @@ object KubernetesUtils extends Logging { opt2.foreach { _ => require(opt1.isEmpty, errMessage) } } - private def getResourceNamePrefix(appName: String, engineRefId: String): String = { - s"$appName-$engineRefId" + private def getResourceNamePrefix(appName: String, engineRefId: Option[String]): String = { + engineRefId.map(refId => s"$appName-$refId").getOrElse(appName) .trim .toLowerCase(Locale.ROOT) .replaceAll("[^a-z0-9\\-]", "-") @@ -130,21 +130,45 @@ object KubernetesUtils extends Logging { .replaceAll("^[0-9]", "x") } - def generateDriverPodName(appName: String, engineRefId: String): String = { - val resolvedResourceName = s"kyuubi-${getResourceNamePrefix(appName, engineRefId)}-driver" - if (resolvedResourceName.length <= DRIVER_POD_NAME_MAX_LENGTH) { - resolvedResourceName + def generateDriverPodName( + appName: String, + engineRefId: String, + forciblyRewrite: Boolean): String = { + val resourceNamePrefix = if (appName.contains(engineRefId)) { + getResourceNamePrefix(appName, None) + } else { + getResourceNamePrefix(appName, Some(engineRefId)) + } + val resolvedResourceName = if (resourceNamePrefix.startsWith("kyuubi-")) { + s"$resourceNamePrefix-driver" } else { + s"kyuubi-$resourceNamePrefix-driver" + } + if (forciblyRewrite || resolvedResourceName.length > DRIVER_POD_NAME_MAX_LENGTH) { s"kyuubi-$engineRefId-driver" + } else { + resolvedResourceName } } - def generateExecutorPodNamePrefix(appName: String, engineRefId: String): String = { - val resolvedResourceName = s"kyuubi-${getResourceNamePrefix(appName, engineRefId)}" - if (resolvedResourceName.length <= EXECUTOR_POD_NAME_PREFIX_MAX_LENGTH) { - resolvedResourceName + def generateExecutorPodNamePrefix( + appName: String, + engineRefId: String, + forciblyRewrite: Boolean): String = { + val resourceNamePrefix = if (appName.contains(engineRefId)) { + getResourceNamePrefix(appName, None) + } else { + getResourceNamePrefix(appName, Some(engineRefId)) + } + val resolvedResourceName = if (resourceNamePrefix.startsWith("kyuubi-")) { + s"$resourceNamePrefix" } else { + s"kyuubi-$resourceNamePrefix" + } + if (forciblyRewrite || resolvedResourceName.length > EXECUTOR_POD_NAME_PREFIX_MAX_LENGTH) { s"kyuubi-$engineRefId" + } else { + resolvedResourceName } } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/WithKyuubiServerOnYarn.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/WithKyuubiServerOnYarn.scala index 012f4df1608..5a674d98fd0 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/WithKyuubiServerOnYarn.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/WithKyuubiServerOnYarn.scala @@ -206,7 +206,7 @@ class KyuubiOperationYarnClusterSuite extends WithKyuubiServerOnYarn with HiveJD } val elapsedTime = System.currentTimeMillis() - startTime assert(elapsedTime < 60 * 1000) - assert(exception.getMessage contains "The engine application has been terminated.") + assert(exception.getMessage contains "Could not open client transport with JDBC Uri") } } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala index 8c0806ba3f5..75226a8b614 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala @@ -125,12 +125,12 @@ class AllKyuubiConfiguration extends KyuubiFunSuite { | override all settings in `$SPARK_HOME/conf/spark-defaults.conf`""" += "### Via JDBC Connection URL" += """ Setting them in the JDBC Connection URL - | supplies session-specific for each SQL engine. For example: - | ``` - |jdbc:hive2://localhost:10009/default;# - |spark.sql.shuffle.partitions=2;spark.executor.memory=5g - |``` - |""" += + | supplies session-specific for each SQL engine. For example:""" ++= + // scalastyle:off + """``` + |jdbc:hive2://localhost:10009/default;#spark.sql.shuffle.partitions=2;spark.executor.memory=5g + |```""" += + // scalastyle:on "" += "- **Runtime SQL Configuration**" += """ - For [Runtime SQL Configurations]( @@ -168,11 +168,14 @@ class AllKyuubiConfiguration extends KyuubiFunSuite { |```""" += """The below options in `kyuubi-defaults.conf` will set `parallelism.default: 2` | and `taskmanager.memory.process.size: 5g` into flink configurations.""" += - "### Via JDBC Connection URL" += - """Setting them in the JDBC Connection URL supplies session-specific - | for each SQL engine. For example: ```jdbc:hive2://localhost:10009/default; - |#parallelism.default=2;taskmanager.memory.process.size=5g``` - |""" += + "### Via JDBC Connection URL" ++= + "Setting them in the JDBC Connection URL supplies session-specific for each SQL engine." + + " For example:" ++= + // scalastyle:off + """``` + | jdbc:hive2://localhost:10009/default;#flink.parallelism.default=2;flink.taskmanager.memory.process.size=5g + |```""" += + // scalastyle:on "### Via SET Statements" += """Please refer to the Flink official online documentation for [SET Statements] |(https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/sql/set/)""" @@ -199,10 +202,14 @@ class AllKyuubiConfiguration extends KyuubiFunSuite { """The below options in `kyuubi-defaults.conf` will set `query_max_stage_count: 500` | and `parse_decimal_literals_as_double: true` into trino session properties.""" += "### Via JDBC Connection URL" += - """Setting them in the JDBC Connection URL supplies session-specific - | for each SQL engine. For example: ```jdbc:hive2://localhost:10009/default; - |#trino.query_max_stage_count=500;trino.parse_decimal_literals_as_double=true``` + "Setting them in the JDBC Connection URL supplies session-specific for each SQL engine." + + " For example:" ++= + // scalastyle:off + """ ``` + | jdbc:hive2://localhost:10009/default;#trino.query_max_stage_count=500;trino.parse_decimal_literals_as_double=true + | ``` |""" += + // scalastyle:on "### Via SET Statements" += """Please refer to the Trino official online documentation for [SET Statements] |(https://trino.io/docs/current/sql/set-session.html)""" diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/JpsApplicationOperationSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/JpsApplicationOperationSuite.scala index a0914afcf0d..bdb0fa787fb 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/JpsApplicationOperationSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/JpsApplicationOperationSuite.scala @@ -83,6 +83,8 @@ class JpsApplicationOperationSuite extends KyuubiFunSuite { val desc1 = jps.getApplicationInfoByTag(ApplicationManagerInfo(None), id) assert(desc1.id != null) assert(desc1.name != null) + assert(!desc1.name.contains("org.apache.spark.launcher.Main")) + assert(desc1.name.contains("org.apache.spark.deploy.SparkSubmit")) assert(desc1.state == ApplicationState.RUNNING) val response = jps.killApplicationByTag(ApplicationManagerInfo(None), id) assert(response._1, response._2) diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilderSuite.scala index 26e355a87bd..84be010ed4b 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilderSuite.scala @@ -81,8 +81,11 @@ class FlinkProcessBuilderSuite extends KyuubiFunSuite { val actualCommands = builder.toString val classpathStr = constructClasspathStr(builder) val expectedCommands = - s"$javaPath -Xmx512m -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 " + - s"-cp $classpathStr $mainClassStr \\\\\\n\\t--conf kyuubi.session.user=vinoyang $confStr" + s"""$javaPath \\\\ + |\\t-Xmx512m \\\\ + |\\t-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 \\\\ + |\\t-cp $classpathStr $mainClassStr \\\\ + |\\t--conf kyuubi.session.user=vinoyang $confStr""".stripMargin val regex = new Regex(expectedCommands) val matcher = regex.pattern.matcher(actualCommands) assert(matcher.matches()) @@ -90,19 +93,20 @@ class FlinkProcessBuilderSuite extends KyuubiFunSuite { private def matchActualAndExpectedApplicationMode(builder: FlinkProcessBuilder): Unit = { val actualCommands = builder.toString + // scalastyle:off line.size.limit val expectedCommands = - escapePaths(s"${builder.flinkExecutable} run-application ") + - s"-t yarn-application " + - s"-Dyarn.ship-files=.*\\/flink-sql-client.*jar;.*\\/flink-sql-gateway.*jar;$tempUdfJar" + - s";.*\\/hive-site\\.xml " + - s"-Dyarn\\.application\\.name=kyuubi_.* " + - s"-Dyarn\\.tags=KYUUBI " + - s"-Dcontainerized\\.master\\.env\\.FLINK_CONF_DIR=\\. " + - s"-Dcontainerized\\.master\\.env\\.HIVE_CONF_DIR=\\. " + - s"-Dexecution.target=yarn-application " + - s"-c org\\.apache\\.kyuubi\\.engine\\.flink\\.FlinkSQLEngine " + - s".*kyuubi-flink-sql-engine_.*jar" + - s"(?: \\\\\\n\\t--conf \\S+=\\S+)+" + escapePaths( + s"""${builder.flinkExecutable} run-application \\\\ + |\\t-t yarn-application \\\\ + |\\t-Dyarn.ship-files=.*flink-sql-client.*jar;.*flink-sql-gateway.*jar;$tempUdfJar;.*hive-site.xml \\\\ + |\\t-Dyarn.application.name=kyuubi_.* \\\\ + |\\t-Dyarn.tags=KYUUBI \\\\ + |\\t-Dcontainerized.master.env.FLINK_CONF_DIR=. \\\\ + |\\t-Dcontainerized.master.env.HIVE_CONF_DIR=. \\\\ + |\\t-Dexecution.target=yarn-application \\\\ + |\\t-c org.apache.kyuubi.engine.flink.FlinkSQLEngine .*kyuubi-flink-sql-engine_.*jar""".stripMargin + + "(?: \\\\\\n\\t--conf \\S+=\\S+)+") + // scalastyle:on line.size.limit val regex = new Regex(expectedCommands) val matcher = regex.pattern.matcher(actualCommands) assert(matcher.matches()) diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilderSuite.scala index bb9884dfa4b..a2f39633ca4 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilderSuite.scala @@ -30,18 +30,18 @@ class HiveProcessBuilderSuite extends KyuubiFunSuite { override def env: Map[String, String] = super.env + (HIVE_HADOOP_CLASSPATH_KEY -> "/hadoop") } val commands = builder.toString.split('\n') - assert(commands.head.endsWith("bin/java"), "wrong exec") - assert(builder.toString.contains("--conf\nkyuubi.session.user=kyuubi")) + assert(commands.head.contains("bin/java"), "wrong exec") + assert(builder.toString.contains("--conf kyuubi.session.user=kyuubi")) assert(commands.exists(ss => ss.contains("kyuubi-hive-sql-engine")), "wrong classpath") - assert(builder.toString.contains("--conf\nkyuubi.on=off")) + assert(builder.toString.contains("--conf kyuubi.on=off")) } test("default engine memory") { val conf = KyuubiConf() .set(ENGINE_HIVE_EXTRA_CLASSPATH, "/hadoop") val builder = new HiveProcessBuilder("kyuubi", conf) - val commands = builder.toString.split('\n') - assert(commands.contains("-Xmx1g")) + val command = builder.toString + assert(command.contains("-Xmx1g")) } test("set engine memory") { @@ -49,8 +49,8 @@ class HiveProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_HIVE_MEMORY, "5g") .set(ENGINE_HIVE_EXTRA_CLASSPATH, "/hadoop") val builder = new HiveProcessBuilder("kyuubi", conf) - val commands = builder.toString.split('\n') - assert(commands.contains("-Xmx5g")) + val command = builder.toString + assert(command.contains("-Xmx5g")) } test("set engine java opts") { @@ -60,8 +60,8 @@ class HiveProcessBuilderSuite extends KyuubiFunSuite { ENGINE_HIVE_JAVA_OPTIONS, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") val builder = new HiveProcessBuilder("kyuubi", conf) - val commands = builder.toString.split('\n') - assert(commands.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) + val command = builder.toString + assert(command.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) } test("set engine extra classpath") { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilderSuite.scala index f85e363d39e..2be39d0f319 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilderSuite.scala @@ -27,13 +27,13 @@ class JdbcProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_JDBC_CONNECTION_URL.key, "") .set(ENGINE_JDBC_CONNECTION_PASSWORD.key, "123456") val builder = new JdbcProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.head.endsWith("bin/java"), "wrong exec") - assert(builder.toString.contains("--conf\nkyuubi.session.user=kyuubi")) - assert(commands.exists(ss => ss.contains("kyuubi-jdbc-engine")), "wrong classpath") - assert(builder.toString.contains("--conf\nkyuubi.on=off")) - assert(builder.toString.contains( - "--conf\nkyuubi.engine.jdbc.connection.password=*********(redacted)")) + val command = builder.toString + assert(command.contains("bin/java"), "wrong exec") + assert(command.contains("--conf kyuubi.session.user=kyuubi")) + assert(command.contains("kyuubi-jdbc-engine"), "wrong classpath") + assert(command.contains("--conf kyuubi.on=off")) + assert(command.contains( + "--conf kyuubi.engine.jdbc.connection.password=*********(redacted)")) } test("capture error from jdbc process builder") { @@ -47,8 +47,8 @@ class JdbcProcessBuilderSuite extends KyuubiFunSuite { val conf = KyuubiConf() .set(ENGINE_JDBC_CONNECTION_URL.key, "") val builder = new JdbcProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-Xmx1g")) + val command = builder.toString + assert(command.contains("-Xmx1g")) } test("set engine memory") { @@ -56,8 +56,8 @@ class JdbcProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_JDBC_MEMORY, "5g") .set(ENGINE_JDBC_CONNECTION_URL.key, "") val builder = new JdbcProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-Xmx5g")) + val command = builder.toString + assert(command.contains("-Xmx5g")) } test("set engine java options") { @@ -67,8 +67,8 @@ class JdbcProcessBuilderSuite extends KyuubiFunSuite { ENGINE_JDBC_JAVA_OPTIONS, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") val builder = new JdbcProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) + val command = builder.toString + assert(command.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) } test("set extra classpath") { @@ -76,7 +76,7 @@ class JdbcProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_JDBC_CONNECTION_URL.key, "") .set(ENGINE_JDBC_EXTRA_CLASSPATH, "/dummy_classpath/*") val builder = new JdbcProcessBuilder("kyuubi", conf) - val commands = builder.toString - assert(commands.contains("/dummy_classpath/*")) + val command = builder.toString + assert(command.contains("/dummy_classpath/*")) } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/InitializeSQLSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/InitializeSQLSuite.scala index 10d662467cf..e119d980266 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/InitializeSQLSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/InitializeSQLSuite.scala @@ -19,19 +19,19 @@ package org.apache.kyuubi.engine.spark import org.apache.kyuubi.WithKyuubiServer import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.{ENGINE_INITIALIZE_SQL, ENGINE_SESSION_INITIALIZE_SQL} +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SESSION_SPARK_INITIALIZE_SQL, ENGINE_SPARK_INITIALIZE_SQL} import org.apache.kyuubi.operation.HiveJDBCTestHelper class InitializeSQLSuite extends WithKyuubiServer with HiveJDBCTestHelper { override protected val conf: KyuubiConf = { KyuubiConf() .set( - ENGINE_INITIALIZE_SQL.key, + ENGINE_SPARK_INITIALIZE_SQL.key, "CREATE DATABASE IF NOT EXISTS INIT_DB;" + "CREATE TABLE IF NOT EXISTS INIT_DB.test(a int) USING CSV;" + "INSERT OVERWRITE TABLE INIT_DB.test VALUES (1);") .set( - ENGINE_SESSION_INITIALIZE_SQL.key, + ENGINE_SESSION_SPARK_INITIALIZE_SQL.key, "CREATE DATABASE IF NOT EXISTS INIT_DB;" + "CREATE TABLE IF NOT EXISTS INIT_DB.test(a int) USING CSV;" + "INSERT INTO INIT_DB.test VALUES (2);") diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/PySparkTests.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/PySparkTests.scala index 16a7f728ea6..c723dcf4aa8 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/PySparkTests.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/PySparkTests.scala @@ -132,6 +132,61 @@ class PySparkTests extends WithKyuubiServer with HiveJDBCTestHelper { }) } + test("Support python magic syntax for python notebook") { + checkPythonRuntimeAndVersion() + withSessionConf()(Map(KyuubiConf.ENGINE_SPARK_PYTHON_MAGIC_ENABLED.key -> "true"))() { + withMultipleConnectionJdbcStatement()({ stmt => + val statement = stmt.asInstanceOf[KyuubiStatement] + statement.executePython("x = [[1, 'a'], [3, 'b']]") + + val resultSet1 = statement.executePython("%json x") + assert(resultSet1.next()) + val output1 = resultSet1.getString("output") + assert(output1 == "{\"application/json\":[[1,\"a\"],[3,\"b\"]]}") + + val resultSet2 = statement.executePython("%table x") + assert(resultSet2.next()) + val output2 = resultSet2.getString("output") + assert(output2 == "{\"application/vnd.livy.table.v1+json\":{" + + "\"headers\":[" + + "{\"name\":\"0\",\"type\":\"INT_TYPE\"},{\"name\":\"1\",\"type\":\"STRING_TYPE\"}" + + "]," + + "\"data\":[" + + "[1,\"a\"],[3,\"b\"]" + + "]}}") + + Seq("table", "json", "matplot").foreach { magic => + val e = intercept[KyuubiSQLException] { + statement.executePython(s"%$magic invalid_value") + }.getMessage + assert(e.contains("KeyError: 'invalid_value'")) + } + + statement.executePython("y = [[1, 2], [3, 'b']]") + var e = intercept[KyuubiSQLException] { + statement.executePython("%table y") + }.getMessage + assert(e.contains("table rows have different types")) + + e = intercept[KyuubiSQLException] { + statement.executePython("%magic_unknown") + }.getMessage + assert(e.contains("unknown magic command 'magic_unknown'")) + }) + } + + withSessionConf()(Map(KyuubiConf.ENGINE_SPARK_PYTHON_MAGIC_ENABLED.key -> "false"))() { + withMultipleConnectionJdbcStatement()({ stmt => + val statement = stmt.asInstanceOf[KyuubiStatement] + statement.executePython("x = [[1, 'a'], [3, 'b']]") + val e = intercept[KyuubiSQLException] { + statement.executePython("%json x") + }.getMessage + assert(e.contains("SyntaxError: invalid syntax")) + }) + } + } + private def runPySparkTest( pyCode: String, output: String): Unit = { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilderSuite.scala index 408f42f6404..8cbbed5af40 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/spark/SparkProcessBuilderSuite.scala @@ -28,13 +28,14 @@ import org.scalatestplus.mockito.MockitoSugar import org.apache.kyuubi._ import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.{ENGINE_LOG_TIMEOUT, ENGINE_SPARK_MAIN_RESOURCE} +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_LOG_TIMEOUT, ENGINE_SPARK_MAIN_RESOURCE, KUBERNETES_FORCIBLY_REWRITE_DRIVER_POD_NAME, KUBERNETES_FORCIBLY_REWRITE_EXEC_POD_NAME_PREFIX} import org.apache.kyuubi.engine.ProcBuilder.KYUUBI_ENGINE_LOG_PATH_KEY import org.apache.kyuubi.engine.spark.SparkProcessBuilder._ import org.apache.kyuubi.ha.HighAvailabilityConf import org.apache.kyuubi.ha.client.AuthTypes import org.apache.kyuubi.service.ServiceUtils import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.command.CommandLineUtils._ class SparkProcessBuilderSuite extends KerberizedTestHelper with MockitoSugar { private def conf = KyuubiConf().set("kyuubi.on", "off") @@ -336,6 +337,15 @@ class SparkProcessBuilderSuite extends KerberizedTestHelper with MockitoSugar { val conf4 = Map(APP_KEY -> chineseAppName) val driverPodName4 = processBuilder.appendPodNameConf(conf4).get(KUBERNETES_DRIVER_POD_NAME) assert(driverPodName4 === Some(s"kyuubi-test-$engineRefId-driver")) + val newProcessBuilder = new SparkProcessBuilder( + "kyuubi", + conf.set(MASTER_KEY, "k8s://internal").set(DEPLOY_MODE_KEY, "cluster").set( + KUBERNETES_FORCIBLY_REWRITE_DRIVER_POD_NAME, + true), + engineRefId) + val conf5 = Map(APP_KEY -> "test-forcibly-rewrite-app") + val driverPodName5 = newProcessBuilder.appendPodNameConf(conf5).get(KUBERNETES_DRIVER_POD_NAME) + assert(driverPodName5 === Some(s"kyuubi-$engineRefId-driver")) } test("[KYUUBI #5165] Test SparkProcessBuilder#appendExecutorPodPrefix") { @@ -363,6 +373,16 @@ class SparkProcessBuilderSuite extends KerberizedTestHelper with MockitoSugar { val execPodNamePrefix3 = processBuilder .appendPodNameConf(conf3).get(KUBERNETES_EXECUTOR_POD_NAME_PREFIX) assert(execPodNamePrefix3 === Some(s"kyuubi-$engineRefId")) + val newProcessBuilder = new SparkProcessBuilder( + "kyuubi", + conf.set(MASTER_KEY, "k8s://internal").set(DEPLOY_MODE_KEY, "cluster").set( + KUBERNETES_FORCIBLY_REWRITE_EXEC_POD_NAME_PREFIX, + true), + engineRefId) + val conf5 = Map(APP_KEY -> "test-forcibly-rewrite-app") + val execPodNamePrefix4 = newProcessBuilder + .appendPodNameConf(conf5).get(KUBERNETES_EXECUTOR_POD_NAME_PREFIX) + assert(execPodNamePrefix4 === Some(s"kyuubi-$engineRefId")) } test("extract spark core scala version") { @@ -404,9 +424,23 @@ class SparkProcessBuilderSuite extends KerberizedTestHelper with MockitoSugar { } } } + + test("default spark.yarn.maxAppAttempts conf in yarn mode") { + val conf1 = KyuubiConf(false) + conf1.set("spark.master", "k8s://test:12345") + val builder1 = new SparkProcessBuilder("", conf1) + val commands1 = builder1.toString.split(' ') + assert(!commands1.contains("spark.yarn.maxAppAttempts")) + + val conf2 = KyuubiConf(false) + conf2.set("spark.master", "yarn") + val builder2 = new SparkProcessBuilder("", conf2) + val commands2 = builder2.toString.split(' ') + assert(commands2.contains("spark.yarn.maxAppAttempts=1")) + } } class FakeSparkProcessBuilder(config: KyuubiConf) extends SparkProcessBuilder("fake", config) { - override protected lazy val commands: Array[String] = Array("ls") + override protected lazy val commands: Iterable[String] = Seq("ls") } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilderSuite.scala index 2c37c41bc4b..a4dfad186a1 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilderSuite.scala @@ -30,11 +30,11 @@ class TrinoProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_TRINO_CONNECTION_CATALOG, "dummy_catalog") val builder = new TrinoProcessBuilder("kyuubi", conf) val commands = builder.toString.split("\n") - assert(commands.head.endsWith("java")) - assert(builder.toString.contains(s"--conf\n${KYUUBI_SESSION_USER_KEY}=kyuubi")) - assert(builder.toString.contains(s"--conf\n${ENGINE_TRINO_CONNECTION_URL.key}=dummy_url")) + assert(commands.head.contains("java")) + assert(builder.toString.contains(s"--conf ${KYUUBI_SESSION_USER_KEY}=kyuubi")) + assert(builder.toString.contains(s"--conf ${ENGINE_TRINO_CONNECTION_URL.key}=dummy_url")) assert(builder.toString.contains( - s"--conf\n${ENGINE_TRINO_CONNECTION_CATALOG.key}=dummy_catalog")) + s"--conf ${ENGINE_TRINO_CONNECTION_CATALOG.key}=dummy_catalog")) } test("capture error from trino process builder") { @@ -49,8 +49,8 @@ class TrinoProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_TRINO_CONNECTION_URL, "dummy_url") .set(ENGINE_TRINO_CONNECTION_CATALOG, "dummy_catalog") val builder = new TrinoProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-Xmx1g")) + val command = builder.toString + assert(command.contains("-Xmx1g")) } test("set engine memory") { @@ -59,8 +59,8 @@ class TrinoProcessBuilderSuite extends KyuubiFunSuite { .set(ENGINE_TRINO_CONNECTION_CATALOG, "dummy_catalog") .set(ENGINE_TRINO_MEMORY, "5g") val builder = new TrinoProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-Xmx5g")) + val command = builder.toString + assert(command.contains("-Xmx5g")) } test("set engine java options") { @@ -71,8 +71,8 @@ class TrinoProcessBuilderSuite extends KyuubiFunSuite { ENGINE_TRINO_JAVA_OPTIONS, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") val builder = new TrinoProcessBuilder("kyuubi", conf) - val commands = builder.toString.split("\n") - assert(commands.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) + val command = builder.toString + assert(command.contains("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")) } test("set extra classpath") { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/events/handler/ServerJsonLoggingEventHandlerSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/events/handler/ServerJsonLoggingEventHandlerSuite.scala index 1dc24aeec94..f78d68eaf71 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/events/handler/ServerJsonLoggingEventHandlerSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/events/handler/ServerJsonLoggingEventHandlerSuite.scala @@ -27,7 +27,6 @@ import scala.util.matching.Regex import com.fasterxml.jackson.databind.ObjectMapper import org.apache.hadoop.conf.Configuration import org.apache.hadoop.fs.{FileSystem, Path} -import org.apache.hive.service.rpc.thrift.{TOpenSessionReq, TStatusCode} import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi._ @@ -40,6 +39,7 @@ import org.apache.kyuubi.operation.OperationState._ import org.apache.kyuubi.server.KyuubiServer import org.apache.kyuubi.service.ServiceState import org.apache.kyuubi.session.{KyuubiSessionManager, SessionType} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TOpenSessionReq, TStatusCode} class ServerJsonLoggingEventHandlerSuite extends WithKyuubiServer with HiveJDBCTestHelper with BatchTestHelper { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiIncrementCollectSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiIncrementCollectSuite.scala index be7f0e80856..7e2890c42ee 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiIncrementCollectSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiIncrementCollectSuite.scala @@ -20,11 +20,11 @@ package org.apache.kyuubi.operation import scala.collection.JavaConverters._ import scala.collection.mutable.ArrayBuffer -import org.apache.hive.service.rpc.thrift._ import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.WithKyuubiServer import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ class KyuubiIncrementCollectSuite extends WithKyuubiServer with HiveJDBCTestHelper { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerConnectionSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerConnectionSuite.scala index 97ab21998b9..1324c70d775 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerConnectionSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerConnectionSuite.scala @@ -23,7 +23,6 @@ import java.util.Properties import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift._ import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.{KYUUBI_VERSION, WithKyuubiServer} @@ -35,6 +34,7 @@ import org.apache.kyuubi.jdbc.hive.{KyuubiConnection, KyuubiSQLException} import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.plugin.SessionConfAdvisor import org.apache.kyuubi.session.{KyuubiSessionImpl, KyuubiSessionManager, SessionHandle, SessionType} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift._ /** * UT with Connection level engine shared cost much time, only run basic jdbc tests. diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerUserSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerUserSuite.scala index a67534164bd..de491e03f21 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerUserSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationPerUserSuite.scala @@ -20,7 +20,6 @@ package org.apache.kyuubi.operation import java.util.{Properties, UUID} import org.apache.hadoop.fs.{FileSystem, FileUtil, Path} -import org.apache.hive.service.rpc.thrift.{TExecuteStatementReq, TGetInfoReq, TGetInfoType, TStatusCode} import org.scalatest.time.SpanSugar._ import org.apache.kyuubi.{KYUUBI_VERSION, Utils, WithKyuubiServer, WithSimpleDFSService} @@ -30,6 +29,7 @@ import org.apache.kyuubi.jdbc.KyuubiHiveDriver import org.apache.kyuubi.jdbc.hive.{KyuubiConnection, KyuubiStatement} import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.session.{KyuubiSessionImpl, SessionHandle} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TExecuteStatementReq, TGetInfoReq, TGetInfoType, TStatusCode} import org.apache.kyuubi.util.SemanticVersion import org.apache.kyuubi.zookeeper.ZookeeperConf diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala index 089b756f54f..260264b6797 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala @@ -29,8 +29,8 @@ import org.apache.hadoop.security.UserGroupInformation import org.apache.kyuubi.RestClientTestHelper import org.apache.kyuubi.client.api.v1.dto.{SessionHandle, SessionOpenCount, SessionOpenRequest} import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER import org.apache.kyuubi.server.http.authentication.AuthSchemes +import org.apache.kyuubi.server.http.util.HttpAuthUtils._ import org.apache.kyuubi.service.authentication.InternalSecurityAccessor import org.apache.kyuubi.session.KyuubiSession @@ -52,13 +52,10 @@ class KyuubiRestAuthenticationSuite extends RestClientTestHelper { } test("test with LDAP authorization") { - val encodeAuthorization = new String( - Base64.getEncoder.encode( - s"$ldapUser:$ldapUserPasswd".getBytes()), - "UTF-8") + val response = webTarget.path("api/v1/sessions/count") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader(ldapUser, ldapUserPasswd)) .get() assert(HttpServletResponse.SC_OK == response.getStatus) @@ -67,13 +64,9 @@ class KyuubiRestAuthenticationSuite extends RestClientTestHelper { } test("test with CUSTOM authorization") { - val encodeAuthorization = new String( - Base64.getEncoder.encode( - s"$customUser:$customPasswd".getBytes()), - "UTF-8") val response = webTarget.path("api/v1/sessions/count") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader(customUser, customPasswd)) .get() assert(HttpServletResponse.SC_FORBIDDEN == response.getStatus) @@ -170,7 +163,7 @@ class KyuubiRestAuthenticationSuite extends RestClientTestHelper { "UTF-8") var response = webTarget.path("api/v1/sessions/count") .request() - .header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL.toString} $encodeAuthorization") + .header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL} $encodeAuthorization") .get() assert(HttpServletResponse.SC_OK == response.getStatus) @@ -183,7 +176,7 @@ class KyuubiRestAuthenticationSuite extends RestClientTestHelper { "UTF-8") response = webTarget.path("api/v1/sessions/count") .request() - .header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL.toString} $badAuthorization") + .header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL} $badAuthorization") .get() assert(HttpServletResponse.SC_UNAUTHORIZED == response.getStatus) diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala index e24b79c2cb5..bd7f78e2423 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala @@ -27,15 +27,15 @@ class PluginLoaderSuite extends KyuubiFunSuite { test("SessionConfAdvisor - wrong class") { val conf = new KyuubiConf(false) - assert(PluginLoader.loadSessionConfAdvisor(conf).isInstanceOf[DefaultSessionConfAdvisor]) + assert(PluginLoader.loadSessionConfAdvisor(conf).head.isInstanceOf[DefaultSessionConfAdvisor]) - conf.set(KyuubiConf.SESSION_CONF_ADVISOR, classOf[InvalidSessionConfAdvisor].getName) + conf.set(KyuubiConf.SESSION_CONF_ADVISOR, Seq(classOf[InvalidSessionConfAdvisor].getName)) val msg1 = intercept[KyuubiException] { PluginLoader.loadSessionConfAdvisor(conf) }.getMessage assert(msg1.contains(s"is not a child of '${classOf[SessionConfAdvisor].getName}'")) - conf.set(KyuubiConf.SESSION_CONF_ADVISOR, "non.exists") + conf.set(KyuubiConf.SESSION_CONF_ADVISOR, Seq("non.exists")) val msg2 = intercept[IllegalArgumentException] { PluginLoader.loadSessionConfAdvisor(conf) }.getMessage @@ -44,27 +44,46 @@ class PluginLoaderSuite extends KyuubiFunSuite { test("FileSessionConfAdvisor") { val conf = new KyuubiConf(false) - conf.set(KyuubiConf.SESSION_CONF_ADVISOR, classOf[FileSessionConfAdvisor].getName) + conf.set(KyuubiConf.SESSION_CONF_ADVISOR, Seq(classOf[FileSessionConfAdvisor].getName)) val advisor = PluginLoader.loadSessionConfAdvisor(conf) - val emptyConfig = advisor.getConfOverlay("chris", conf.getAll.asJava) + val emptyConfig = + advisor.map(_.getConfOverlay("chris", conf.getAll.asJava).asScala).reduce(_ ++ _).asJava assert(emptyConfig.isEmpty) conf.set(KyuubiConf.SESSION_CONF_PROFILE, "non.exists") - val nonExistsConfig = advisor.getConfOverlay("chris", conf.getAll.asJava) + val nonExistsConfig = + advisor.map(_.getConfOverlay("chris", conf.getAll.asJava).asScala).reduce(_ ++ _).asJava assert(nonExistsConfig.isEmpty) conf.set(KyuubiConf.SESSION_CONF_PROFILE, "cluster-a") - val clusterAConf = advisor.getConfOverlay("chris", conf.getAll.asJava) + val clusterAConf = + advisor.map(_.getConfOverlay("chris", conf.getAll.asJava).asScala).reduce(_ ++ _).asJava assert(clusterAConf.get("kyuubi.ha.namespace") == "kyuubi-ns-a") assert(clusterAConf.get("kyuubi.zk.ha.namespace") == null) assert(clusterAConf.size() == 5) - val clusterAConfFromCache = advisor.getConfOverlay("chris", conf.getAll.asJava) + val clusterAConfFromCache = + advisor.map(_.getConfOverlay("chris", conf.getAll.asJava).asScala).reduce(_ ++ _).asJava assert(clusterAConfFromCache.get("kyuubi.ha.namespace") == "kyuubi-ns-a") assert(clusterAConfFromCache.get("kyuubi.zk.ha.namespace") == null) assert(clusterAConfFromCache.size() == 5) } + test("SessionConfAdvisor - multi class") { + val conf = new KyuubiConf(false) + conf.set( + KyuubiConf.SESSION_CONF_ADVISOR, + Seq(classOf[FileSessionConfAdvisor].getName, classOf[TestSessionConfAdvisor].getName)) + val advisor = PluginLoader.loadSessionConfAdvisor(conf) + conf.set(KyuubiConf.SESSION_CONF_PROFILE, "cluster-a") + val clusterAConf = + advisor.map(_.getConfOverlay("chris", conf.getAll.asJava).asScala).reduce(_ ++ _).asJava + assert(clusterAConf.get("kyuubi.ha.namespace") == "kyuubi-ns-a") + assert(clusterAConf.get("kyuubi.zk.ha.namespace") == null) + assert(clusterAConf.get("spark.k3") == "v3") + assert(clusterAConf.size() == 7) + } + test("GroupProvider - wrong class") { val conf = new KyuubiConf(false) conf.set(KyuubiConf.GROUP_PROVIDER, "hadoop") @@ -99,3 +118,11 @@ class PluginLoaderSuite extends KyuubiFunSuite { class InvalidSessionConfAdvisor class InvalidGroupProvider + +class TestSessionConfAdvisor extends SessionConfAdvisor { + override def getConfOverlay( + user: String, + sessionConf: java.util.Map[String, String]): java.util.Map[String, String] = { + Map("spark.k3" -> "v3", "spark.k4" -> "v4").asJava + } +} diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendServiceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendServiceSuite.scala index 5c54cbbb4b7..9b41fb067c3 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendServiceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/KyuubiTBinaryFrontendServiceSuite.scala @@ -19,13 +19,13 @@ package org.apache.kyuubi.server import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.{TOpenSessionReq, TSessionHandle} import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.{KyuubiFunSuite, Utils, WithKyuubiServer} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.operation.TClientTestUtils +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TOpenSessionReq, TSessionHandle} class KyuubiTBinaryFrontendServiceSuite extends WithKyuubiServer with KyuubiFunSuite { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala index ea87e3ea0d8..0951d82727c 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala @@ -17,22 +17,21 @@ package org.apache.kyuubi.server.api.v1 -import java.nio.charset.StandardCharsets import java.time.Duration -import java.util.{Base64, UUID} +import java.util.UUID import javax.ws.rs.client.Entity -import javax.ws.rs.core.{GenericType, MediaType} +import javax.ws.rs.core.{GenericType, MediaType, Response} import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 import org.mockito.Mockito.lenient import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.scalatestplus.mockito.MockitoSugar.mock import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite, RestFrontendTestHelper, Utils} -import org.apache.kyuubi.client.api.v1.dto.{Engine, OperationData, ServerData, SessionData, SessionHandle, SessionOpenRequest} +import org.apache.kyuubi.client.api.v1.dto._ import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf._ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_CONNECTION_URL_KEY import org.apache.kyuubi.engine.{ApplicationManagerInfo, ApplicationState, EngineRef, KyuubiApplicationManager} import org.apache.kyuubi.engine.EngineType.SPARK_SQL @@ -42,22 +41,20 @@ import org.apache.kyuubi.ha.client.{DiscoveryPaths, ServiceDiscovery} import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient import org.apache.kyuubi.plugin.PluginLoader import org.apache.kyuubi.server.KyuubiRestFrontendService -import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER +import org.apache.kyuubi.server.http.util.HttpAuthUtils +import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER +import org.apache.kyuubi.service.authentication.AnonymousAuthenticationProviderImpl +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { private val engineMgr = new KyuubiApplicationManager() override protected lazy val conf: KyuubiConf = KyuubiConf() - .set(KyuubiConf.SERVER_ADMINISTRATORS, Set("admin001")) - .set(KyuubiConf.ENGINE_IDLE_TIMEOUT, Duration.ofMinutes(3).toMillis) - - private val encodeAuthorization: String = { - new String( - Base64.getEncoder.encode( - s"${Utils.currentUser}:".getBytes()), - StandardCharsets.UTF_8) - } + .set(AUTHENTICATION_METHOD, Set("CUSTOM")) + .set(AUTHENTICATION_CUSTOM_CLASS, classOf[AnonymousAuthenticationProviderImpl].getName) + .set(SERVER_ADMINISTRATORS, Set("admin001")) + .set(ENGINE_IDLE_TIMEOUT, Duration.ofMinutes(3).toMillis) override def beforeAll(): Unit = { super.beforeAll() @@ -74,70 +71,64 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { var response = webTarget.path("api/v1/admin/refresh/hadoop_conf") .request() .post(null) - assert(405 == response.getStatus) + assert(response.getStatus === 401) response = webTarget.path("api/v1/admin/refresh/hadoop_conf") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .post(null) - assert(200 == response.getStatus) + assert(response.getStatus === 200) - val admin001AuthHeader = new String( - Base64.getEncoder.encode("admin001".getBytes()), - StandardCharsets.UTF_8) response = webTarget.path("api/v1/admin/refresh/hadoop_conf") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $admin001AuthHeader") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader("admin001")) .post(null) - assert(200 == response.getStatus) + assert(response.getStatus === 200) - val admin002AuthHeader = new String( - Base64.getEncoder.encode("admin002".getBytes()), - StandardCharsets.UTF_8) response = webTarget.path("api/v1/admin/refresh/hadoop_conf") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $admin002AuthHeader") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader("admin002")) .post(null) - assert(405 == response.getStatus) + assert(response.getStatus === 405) } test("refresh user defaults config of the kyuubi server") { var response = webTarget.path("api/v1/admin/refresh/user_defaults_conf") .request() .post(null) - assert(405 == response.getStatus) + assert(response.getStatus === 401) response = webTarget.path("api/v1/admin/refresh/user_defaults_conf") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .post(null) - assert(200 == response.getStatus) + assert(response.getStatus === 200) } test("refresh unlimited users of the kyuubi server") { var response = webTarget.path("api/v1/admin/refresh/unlimited_users") .request() .post(null) - assert(405 == response.getStatus) + assert(response.getStatus === 401) response = webTarget.path("api/v1/admin/refresh/unlimited_users") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .post(null) - assert(200 == response.getStatus) + assert(response.getStatus === 200) } test("refresh deny users of the kyuubi server") { var response = webTarget.path("api/v1/admin/refresh/deny_users") .request() .post(null) - assert(405 == response.getStatus) + assert(response.getStatus === 401) response = webTarget.path("api/v1/admin/refresh/deny_users") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .post(null) - assert(200 == response.getStatus) + assert(response.getStatus === 200) } test("list/close sessions") { @@ -145,13 +136,15 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { var response = webTarget.path("api/v1/sessions") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(response.getStatus === 200) // get session list var response2 = webTarget.path("api/v1/admin/sessions").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() - assert(200 == response2.getStatus) + assert(response2.getStatus === 200) val sessions1 = response2.readEntity(new GenericType[Seq[SessionData]]() {}) assert(sessions1.nonEmpty) assert(sessions1.head.getConf.get(KYUUBI_SESSION_CONNECTION_URL_KEY) === fe.connectionUrl) @@ -159,13 +152,13 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { // close an opened session val sessionHandle = response.readEntity(classOf[SessionHandle]).getIdentifier response = webTarget.path(s"api/v1/admin/sessions/$sessionHandle").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .delete() - assert(200 == response.getStatus) + assert(response.getStatus === 200) // get session list again response2 = webTarget.path("api/v1/admin/sessions").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() assert(200 == response2.getStatus) val sessions2 = response2.readEntity(classOf[Seq[SessionData]]) @@ -205,26 +198,26 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { var response = webTarget.path("api/v1/admin/sessions") .queryParam("users", "admin") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() var sessions = response.readEntity(classOf[Seq[SessionData]]) - assert(200 == response.getStatus) + assert(response.getStatus === 200) assert(sessions.size == 2) response = webTarget.path("api/v1/admin/sessions") .queryParam("users", "test_user_1,test_user_2") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() sessions = response.readEntity(classOf[Seq[SessionData]]) - assert(200 == response.getStatus) + assert(response.getStatus === 200) assert(sessions.size == 2) // list operations response = webTarget.path("api/v1/admin/operations") .queryParam("users", "test_user_1,test_user_2") .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() var operations = response.readEntity(classOf[Seq[OperationData]]) assert(operations.size == 2) @@ -232,10 +225,10 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { response = webTarget.path("api/v1/admin/operations") .queryParam("sessionHandle", sessionHandle.identifier) .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() operations = response.readEntity(classOf[Seq[OperationData]]) - assert(200 == response.getStatus) + assert(response.getStatus === 200) assert(operations.size == 1) } @@ -250,22 +243,22 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { // list operations var response = webTarget.path("api/v1/admin/operations").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() - assert(200 == response.getStatus) + assert(response.getStatus === 200) var operations = response.readEntity(new GenericType[Seq[OperationData]]() {}) assert(operations.nonEmpty) assert(operations.map(op => op.getIdentifier).contains(operation.identifier.toString)) // close operation response = webTarget.path(s"api/v1/admin/operations/${operation.identifier}").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .delete() - assert(200 == response.getStatus) + assert(response.getStatus === 200) // list again response = webTarget.path("api/v1/admin/operations").request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get() operations = response.readEntity(new GenericType[Seq[OperationData]]() {}) assert(!operations.map(op => op.getIdentifier).contains(operation.identifier.toString)) @@ -297,10 +290,10 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { .queryParam("sharelevel", "USER") .queryParam("type", "spark_sql") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .delete() - assert(200 == response.getStatus) + assert(response.getStatus === 200) assert(client.pathExists(engineSpace)) eventually(timeout(5.seconds), interval(100.milliseconds)) { assert(client.getChildren(engineSpace).isEmpty, s"refId same with $id?") @@ -343,10 +336,10 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { .queryParam("sharelevel", "GROUP") .queryParam("type", "spark_sql") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .delete() - assert(200 == response.getStatus) + assert(response.getStatus === 200) assert(client.pathExists(engineSpace)) eventually(timeout(5.seconds), interval(100.milliseconds)) { assert(client.getChildren(engineSpace).isEmpty, s"refId same with $id?") @@ -387,10 +380,73 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { .queryParam("type", "spark_sql") .queryParam("subdomain", id) .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .delete() - assert(200 == response.getStatus) + assert(response.getStatus === 200) + } + } + + test("delete engine - user share level & proxyUser") { + val normalUser = "kyuubi" + + val id = UUID.randomUUID().toString + conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, USER.toString) + conf.set(KyuubiConf.ENGINE_TYPE, SPARK_SQL.toString) + conf.set(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0) + conf.set(HighAvailabilityConf.HA_NAMESPACE, "kyuubi_test") + conf.set(KyuubiConf.GROUP_PROVIDER, "hadoop") + + // In EngineRef, when use hive.server2.proxy.user or kyuubi.session.proxy.user + // the user is the proxyUser, and in our test it is normalUser + val engine = + new EngineRef(conf.clone, user = normalUser, PluginLoader.loadGroupProvider(conf), id, null) + + // so as the firstChild in engineSpace we use normalUser + val engineSpace = DiscoveryPaths.makePath( + s"kyuubi_test_${KYUUBI_VERSION}_USER_SPARK_SQL", + normalUser, + "default") + + withDiscoveryClient(conf) { client => + engine.getOrCreate(client) + + assert(client.pathExists(engineSpace)) + assert(client.getChildren(engineSpace).size == 1) + + def runDeleteEngine( + kyuubiProxyUser: Option[String], + hs2ProxyUser: Option[String]): Response = { + var internalWebTarget = webTarget.path("api/v1/admin/engine") + .queryParam("sharelevel", "USER") + .queryParam("type", "SPARK_SQL") + + kyuubiProxyUser.map(username => + internalWebTarget = internalWebTarget.queryParam("proxyUser", username)) + hs2ProxyUser.map(username => + internalWebTarget = internalWebTarget.queryParam("hive.server2.proxy.user", username)) + + internalWebTarget.request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader("anonymous")) + .delete() + } + + // use proxyUser + val deleteEngineResponse1 = runDeleteEngine(Option(normalUser), None) + assert(deleteEngineResponse1.getStatus === 405) + val errorMessage = s"Failed to validate proxy privilege of anonymous for $normalUser" + assert(deleteEngineResponse1.readEntity(classOf[String]).contains(errorMessage)) + + // it should be the same behavior as hive.server2.proxy.user + val deleteEngineResponse2 = runDeleteEngine(None, Option(normalUser)) + assert(deleteEngineResponse2.getStatus === 405) + assert(deleteEngineResponse2.readEntity(classOf[String]).contains(errorMessage)) + + // when both set, proxyUser takes precedence + val deleteEngineResponse3 = + runDeleteEngine(Option(normalUser), Option(s"${normalUser}HiveServer2")) + assert(deleteEngineResponse3.getStatus === 405) + assert(deleteEngineResponse3.readEntity(classOf[String]).contains(errorMessage)) } } @@ -419,10 +475,10 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val response = webTarget.path("api/v1/admin/engine") .queryParam("type", "spark_sql") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get - assert(200 == response.getStatus) + assert(response.getStatus === 200) val engines = response.readEntity(new GenericType[Seq[Engine]]() {}) assert(engines.size == 1) assert(engines(0).getEngineType == "SPARK_SQL") @@ -465,10 +521,10 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val response = webTarget.path("api/v1/admin/engine") .queryParam("type", "spark_sql") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get - assert(200 == response.getStatus) + assert(response.getStatus === 200) val engines = response.readEntity(new GenericType[Seq[Engine]]() {}) assert(engines.size == 1) assert(engines(0).getEngineType == "SPARK_SQL") @@ -524,9 +580,9 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val response = webTarget.path("api/v1/admin/engine") .queryParam("type", "spark_sql") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get - assert(200 == response.getStatus) + assert(response.getStatus === 200) val result = response.readEntity(new GenericType[Seq[Engine]]() {}) assert(result.size == 2) @@ -534,7 +590,7 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { .queryParam("type", "spark_sql") .queryParam("subdomain", id1) .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get assert(200 == response1.getStatus) val result1 = response1.readEntity(new GenericType[Seq[Engine]]() {}) @@ -552,53 +608,25 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { } } - test("list server") { - // Mock Kyuubi Server - val serverDiscovery = mock[ServiceDiscovery] - lenient.when(serverDiscovery.fe).thenReturn(fe) - val namespace = conf.get(HighAvailabilityConf.HA_NAMESPACE) - withDiscoveryClient(conf) { client => - client.registerService(conf, namespace, serverDiscovery) - - val response = webTarget.path("api/v1/admin/server") - .request() - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") - .get - - assert(200 == response.getStatus) - val result = response.readEntity(new GenericType[Seq[ServerData]]() {}) - assert(result.size == 1) - val testServer = result.head - val restFrontendService = fe.asInstanceOf[KyuubiRestFrontendService] - - assert(namespace.equals(testServer.getNamespace.replaceFirst("/", ""))) - assert(restFrontendService.host.equals(testServer.getHost)) - assert(restFrontendService.connectionUrl.equals(testServer.getInstance())) - assert(!testServer.getAttributes.isEmpty) - val attributes = testServer.getAttributes - assert(attributes.containsKey("serviceUri") && - attributes.get("serviceUri").equals(fe.connectionUrl)) - assert(attributes.containsKey("version")) - assert(attributes.containsKey("sequence")) - assert("Running".equals(testServer.getStatus)) - } - } + test("list engine - user share level & proxyUser") { + val normalUser = "kyuubi" - test("list all engine - user share level") { val id = UUID.randomUUID().toString conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, USER.toString) conf.set(KyuubiConf.ENGINE_TYPE, SPARK_SQL.toString) conf.set(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0) conf.set(HighAvailabilityConf.HA_NAMESPACE, "kyuubi_test") - conf.set(KyuubiConf.ENGINE_IDLE_TIMEOUT, 180000L) conf.set(KyuubiConf.GROUP_PROVIDER, "hadoop") + // In EngineRef, when use hive.server2.proxy.user or kyuubi.session.proxy.user + // the user is the proxyUser, and in our test it is normalUser val engine = - new EngineRef(conf.clone, Utils.currentUser, PluginLoader.loadGroupProvider(conf), id, null) + new EngineRef(conf.clone, user = normalUser, PluginLoader.loadGroupProvider(conf), id, null) + // so as the firstChild in engineSpace we use normalUser val engineSpace = DiscoveryPaths.makePath( s"kyuubi_test_${KYUUBI_VERSION}_USER_SPARK_SQL", - Utils.currentUser, + normalUser, "") withDiscoveryClient(conf) { client => @@ -607,131 +635,71 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { assert(client.pathExists(engineSpace)) assert(client.getChildren(engineSpace).size == 1) - val response = webTarget.path("api/v1/admin/engine") - .queryParam("all", "true") - .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") - .get - - assert(200 == response.getStatus) - val engines = response.readEntity(new GenericType[Seq[Engine]]() {}) - assert(engines.size == 1) - assert(engines(0).getEngineType == "SPARK_SQL") - assert(engines(0).getSharelevel == "USER") - assert(engines(0).getSubdomain == "default") - - // kill the engine application - engineMgr.killApplication(ApplicationManagerInfo(None), id) - eventually(timeout(30.seconds), interval(100.milliseconds)) { - assert(engineMgr.getApplicationInfo(ApplicationManagerInfo(None), id).exists( - _.state == ApplicationState.NOT_FOUND)) + def runListEngine(kyuubiProxyUser: Option[String], hs2ProxyUser: Option[String]): Response = { + var internalWebTarget = webTarget.path("api/v1/admin/engine") + .queryParam("sharelevel", "USER") + .queryParam("type", "SPARK_SQL") + + kyuubiProxyUser.map { username => + internalWebTarget = internalWebTarget.queryParam("proxyUser", username) + } + hs2ProxyUser.map { username => + internalWebTarget = internalWebTarget.queryParam("hive.server2.proxy.user", username) + } + + internalWebTarget.request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader("anonymous")) + .get } - } - } - - test("list all engines - group share level") { - val id = UUID.randomUUID().toString - conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, GROUP.toString) - conf.set(KyuubiConf.ENGINE_TYPE, SPARK_SQL.toString) - conf.set(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0) - conf.set(HighAvailabilityConf.HA_NAMESPACE, "kyuubi_test") - conf.set(KyuubiConf.ENGINE_IDLE_TIMEOUT, 180000L) - conf.set(KyuubiConf.GROUP_PROVIDER, "hadoop") - - val engine = - new EngineRef(conf.clone, Utils.currentUser, PluginLoader.loadGroupProvider(conf), id, null) - - val engineSpace = DiscoveryPaths.makePath( - s"kyuubi_test_${KYUUBI_VERSION}_GROUP_SPARK_SQL", - fe.asInstanceOf[KyuubiRestFrontendService].sessionManager.groupProvider.primaryGroup( - Utils.currentUser, - null), - "") - withDiscoveryClient(conf) { client => - engine.getOrCreate(client) - - assert(client.pathExists(engineSpace)) - assert(client.getChildren(engineSpace).size == 1) - - val response = webTarget.path("api/v1/admin/engine") - .queryParam("all", "true") - .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") - .get - - assert(200 == response.getStatus) - val engines = response.readEntity(new GenericType[Seq[Engine]]() {}) - assert(engines.size == 1) - assert(engines(0).getEngineType == "SPARK_SQL") - assert(engines(0).getSharelevel == "GROUP") - assert(engines(0).getSubdomain == "default") - - // kill the engine application - engineMgr.killApplication(ApplicationManagerInfo(None), id) - eventually(timeout(30.seconds), interval(100.milliseconds)) { - assert(engineMgr.getApplicationInfo(ApplicationManagerInfo(None), id).exists( - _.state == ApplicationState.NOT_FOUND)) - } + // use proxyUser + val listEngineResponse1 = runListEngine(Option(normalUser), None) + assert(listEngineResponse1.getStatus === 405) + val errorMessage = s"Failed to validate proxy privilege of anonymous for $normalUser" + assert(listEngineResponse1.readEntity(classOf[String]).contains(errorMessage)) + + // it should be the same behavior as hive.server2.proxy.user + val listEngineResponse2 = runListEngine(None, Option(normalUser)) + assert(listEngineResponse2.getStatus === 405) + assert(listEngineResponse2.readEntity(classOf[String]).contains(errorMessage)) + + // when both set, proxyUser takes precedence + val listEngineResponse3 = + runListEngine(Option(normalUser), Option(s"${normalUser}HiveServer2")) + assert(listEngineResponse3.getStatus === 405) + assert(listEngineResponse3.readEntity(classOf[String]).contains(errorMessage)) } } - test("list all engines - connection share level") { - conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, CONNECTION.toString) - conf.set(KyuubiConf.ENGINE_TYPE, SPARK_SQL.toString) - conf.set(KyuubiConf.FRONTEND_THRIFT_BINARY_BIND_PORT, 0) - conf.set(HighAvailabilityConf.HA_NAMESPACE, "kyuubi_test") - conf.set(KyuubiConf.ENGINE_IDLE_TIMEOUT, 180000L) - conf.set(KyuubiConf.GROUP_PROVIDER, "hadoop") - - val engineSpace = DiscoveryPaths.makePath( - s"kyuubi_test_${KYUUBI_VERSION}_CONNECTION_SPARK_SQL", - Utils.currentUser, - "") - - val id1 = UUID.randomUUID().toString - val engine1 = - new EngineRef(conf.clone, Utils.currentUser, PluginLoader.loadGroupProvider(conf), id1, null) - val engineSpace1 = DiscoveryPaths.makePath( - s"kyuubi_test_${KYUUBI_VERSION}_CONNECTION_SPARK_SQL", - Utils.currentUser, - id1) - - val id2 = UUID.randomUUID().toString - val engine2 = - new EngineRef(conf.clone, Utils.currentUser, PluginLoader.loadGroupProvider(conf), id2, null) - val engineSpace2 = DiscoveryPaths.makePath( - s"kyuubi_test_${KYUUBI_VERSION}_CONNECTION_SPARK_SQL", - Utils.currentUser, - id2) - + test("list server") { + // Mock Kyuubi Server + val serverDiscovery = mock[ServiceDiscovery] + lenient.when(serverDiscovery.fe).thenReturn(fe) + val namespace = conf.get(HighAvailabilityConf.HA_NAMESPACE) withDiscoveryClient(conf) { client => - engine1.getOrCreate(client) - engine2.getOrCreate(client) - - assert(client.pathExists(engineSpace)) - assert(client.getChildren(engineSpace).size == 2) - assert(client.pathExists(engineSpace1)) - assert(client.pathExists(engineSpace2)) + client.registerService(conf, namespace, serverDiscovery) - val response = webTarget.path("api/v1/admin/engine") - .queryParam("all", "true") - .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + val response = webTarget.path("api/v1/admin/server") + .request() + .header(AUTHORIZATION_HEADER, HttpAuthUtils.basicAuthorizationHeader(Utils.currentUser)) .get - assert(200 == response.getStatus) - val result = response.readEntity(new GenericType[Seq[Engine]]() {}) - assert(result.size == 2) - // kill the engine application - engineMgr.killApplication(ApplicationManagerInfo(None), id1) - engineMgr.killApplication(ApplicationManagerInfo(None), id2) - eventually(timeout(30.seconds), interval(100.milliseconds)) { - assert(engineMgr.getApplicationInfo(ApplicationManagerInfo(None), id1) - .exists(_.state == ApplicationState.NOT_FOUND)) - assert(engineMgr.getApplicationInfo(ApplicationManagerInfo(None), id2) - .exists(_.state == ApplicationState.NOT_FOUND)) - } + assert(response.getStatus === 200) + val result = response.readEntity(new GenericType[Seq[ServerData]]() {}) + assert(result.size == 1) + val testServer = result.head + val restFrontendService = fe.asInstanceOf[KyuubiRestFrontendService] + + assert(namespace.equals(testServer.getNamespace.replaceFirst("/", ""))) + assert(restFrontendService.host.equals(testServer.getHost)) + assert(restFrontendService.connectionUrl.equals(testServer.getInstance())) + assert(!testServer.getAttributes.isEmpty) + val attributes = testServer.getAttributes + assert(attributes.containsKey("serverUri") && + attributes.get("serverUri").equals(fe.connectionUrl)) + assert(attributes.containsKey("version")) + assert(attributes.containsKey("sequence")) + assert("Running".equals(testServer.getStatus)) } } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/BatchesResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/BatchesResourceSuite.scala index 7270f68d6b7..6ae2bd04063 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/BatchesResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/BatchesResourceSuite.scala @@ -19,15 +19,15 @@ package org.apache.kyuubi.server.api.v1 import java.net.InetAddress import java.nio.file.Paths -import java.util.{Base64, UUID} +import java.util.UUID import javax.ws.rs.client.Entity import javax.ws.rs.core.{MediaType, Response} import scala.collection.JavaConverters._ +import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.DurationInt -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.glassfish.jersey.media.multipart.FormDataMultiPart import org.glassfish.jersey.media.multipart.file.FileDataBodyPart @@ -43,10 +43,11 @@ import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.operation.{BatchJobSubmission, OperationState} import org.apache.kyuubi.operation.OperationState.OperationState import org.apache.kyuubi.server.{KyuubiBatchService, KyuubiRestFrontendService} -import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER +import org.apache.kyuubi.server.http.util.HttpAuthUtils.{basicAuthorizationHeader, AUTHORIZATION_HEADER} import org.apache.kyuubi.server.metadata.api.{Metadata, MetadataFilter} -import org.apache.kyuubi.service.authentication.{InternalSecurityAccessor, KyuubiAuthenticationFactory} +import org.apache.kyuubi.service.authentication.{AnonymousAuthenticationProviderImpl, KyuubiAuthenticationFactory} import org.apache.kyuubi.session.{KyuubiBatchSession, KyuubiSessionManager, SessionHandle, SessionType} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class BatchesV1ResourceSuite extends BatchesResourceSuiteBase { override def batchVersion: String = "1" @@ -58,8 +59,8 @@ class BatchesV2ResourceSuite extends BatchesResourceSuiteBase { override def batchVersion: String = "2" override def customConf: Map[String, String] = Map( - KyuubiConf.METADATA_REQUEST_ASYNC_RETRY_ENABLED.key -> "false", - KyuubiConf.BATCH_SUBMITTER_ENABLED.key -> "true") + METADATA_REQUEST_ASYNC_RETRY_ENABLED.key -> "false", + BATCH_SUBMITTER_ENABLED.key -> "true") override def afterEach(): Unit = { val sessionManager = fe.be.sessionManager.asInstanceOf[KyuubiSessionManager] @@ -82,23 +83,17 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite def customConf: Map[String, String] override protected lazy val conf: KyuubiConf = { + val testResourceDir = Paths.get(sparkBatchTestResource.get).getParent val kyuubiConf = KyuubiConf() - .set(KyuubiConf.ENGINE_SECURITY_ENABLED, true) - .set(KyuubiConf.ENGINE_SECURITY_SECRET_PROVIDER, "simple") - .set(KyuubiConf.SIMPLE_SECURITY_SECRET_PROVIDER_PROVIDER_SECRET, "ENGINE____SECRET") - .set(KyuubiConf.BATCH_IMPL_VERSION, batchVersion) - .set( - KyuubiConf.SESSION_LOCAL_DIR_ALLOW_LIST, - Set(Paths.get(sparkBatchTestResource.get).getParent.toString)) + .set(AUTHENTICATION_METHOD, Set("CUSTOM")) + .set(AUTHENTICATION_CUSTOM_CLASS, classOf[AnonymousAuthenticationProviderImpl].getName) + .set(SERVER_ADMINISTRATORS, Set("admin")) + .set(BATCH_IMPL_VERSION, batchVersion) + .set(SESSION_LOCAL_DIR_ALLOW_LIST, Set(testResourceDir.toString)) customConf.foreach { case (k, v) => kyuubiConf.set(k, v) } kyuubiConf } - override def beforeAll(): Unit = { - super.beforeAll() - InternalSecurityAccessor.initialize(conf, true) - } - override def afterEach(): Unit = { val sessionManager = fe.be.sessionManager.asInstanceOf[KyuubiSessionManager] sessionManager.allSessions().foreach { session => @@ -115,6 +110,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val response = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) assert(response.getStatus === 200) var batch = response.readEntity(classOf[Batch]) @@ -138,6 +134,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val proxyUserRequest = requestObj val proxyUserResponse = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(proxyUserRequest, MediaType.APPLICATION_JSON_TYPE)) assert(proxyUserResponse.getStatus === 405) var errorMessage = "Failed to validate proxy privilege of anonymous for root" @@ -145,6 +142,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite var getBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(getBatchResponse.getStatus === 200) batch = getBatchResponse.readEntity(classOf[Batch]) @@ -169,6 +167,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite // invalid batchId getBatchResponse = webTarget.path(s"api/v1/batches/invalidBatchId") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(getBatchResponse.getStatus === 404) @@ -180,6 +179,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "1") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() log = logResponse.readEntity(classOf[OperationLog]) assert(log.getRowCount === 1) @@ -193,6 +193,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "-1") .queryParam("size", "100") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() log = logResponse.readEntity(classOf[OperationLog]) if (log.getRowCount > 0) { @@ -206,40 +207,32 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite } // invalid user name - val encodeAuthorization = - new String(Base64.getEncoder.encode(batch.getId.getBytes()), "UTF-8") var deleteBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader(batch.getId)) .delete() assert(deleteBatchResponse.getStatus === 405) - errorMessage = s"${batch.getId} is not allowed to close the session belong to anonymous" + errorMessage = s"Failed to validate proxy privilege of ${batch.getId} for anonymous" assert(deleteBatchResponse.readEntity(classOf[String]).contains(errorMessage)) // invalid batchId deleteBatchResponse = webTarget.path(s"api/v1/batches/notValidUUID") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() assert(deleteBatchResponse.getStatus === 404) // non-existed batch session deleteBatchResponse = webTarget.path(s"api/v1/batches/${UUID.randomUUID().toString}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() assert(deleteBatchResponse.getStatus === 404) - // invalid proxy user - deleteBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") - .queryParam("hive.server2.proxy.user", "invalidProxy") - .request(MediaType.APPLICATION_JSON_TYPE) - .delete() - assert(deleteBatchResponse.getStatus === 405) - errorMessage = "Failed to validate proxy privilege of anonymous for invalidProxy" - assert(deleteBatchResponse.readEntity(classOf[String]).contains(errorMessage)) - // check close batch session deleteBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() assert(deleteBatchResponse.getStatus === 200) val closeBatchResponse = deleteBatchResponse.readEntity(classOf[CloseBatchResponse]) @@ -247,6 +240,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite // check state after close batch session getBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(getBatchResponse.getStatus === 200) batch = getBatchResponse.readEntity(classOf[Batch]) @@ -260,6 +254,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite // close the closed batch session deleteBatchResponse = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() assert(deleteBatchResponse.getStatus === 200) assert(!deleteBatchResponse.readEntity(classOf[CloseBatchResponse]).isSuccess) @@ -275,6 +270,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val response = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(multipart, MediaType.MULTIPART_FORM_DATA)) assert(response.getStatus === 200) val batch = response.readEntity(classOf[Batch]) @@ -297,6 +293,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite eventually(timeout(5.seconds), interval(200.millis)) { val resp = webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() val batchState = resp.readEntity(classOf[Batch]).getState assert(batchState === "PENDING" || batchState === "RUNNING") @@ -304,6 +301,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite webTarget.path(s"api/v1/batches/${batch.getId}") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() eventually(timeout(5.seconds), interval(200.millis)) { assert(KyuubiApplicationManager.uploadWorkDir.toFile.listFiles().isEmpty) @@ -318,6 +316,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val resp1 = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(reqObj, MediaType.APPLICATION_JSON_TYPE)) assert(resp1.getStatus === 200) val batch1 = resp1.readEntity(classOf[Batch]) @@ -325,6 +324,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val resp2 = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(reqObj, MediaType.APPLICATION_JSON_TYPE)) assert(resp2.getStatus === 200) val batch2 = resp2.readEntity(classOf[Batch]) @@ -348,6 +348,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response.getStatus === 200) @@ -402,6 +403,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response2.getStatus === 200) @@ -414,6 +416,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "2") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response3.getStatus === 200) @@ -426,6 +429,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "3") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response4.getStatus === 200) @@ -437,6 +441,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "2") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response5.getStatus === 200) @@ -449,6 +454,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "2") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response6.getStatus === 200) @@ -463,6 +469,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "2") .queryParam("size", "2") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response7.getStatus === 500) } @@ -493,6 +500,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite "resource is a required parameter")).foreach { case (req, msg) => val response = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)) assert(response.getStatus === 500) assert(response.readEntity(classOf[String]).contains(msg)) @@ -506,6 +514,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite "Invalid batchId: 3ea7ddbe-0c35-45da-85ad-3186770181a7")).foreach { case (batchId, msg) => val response = webTarget.path(s"api/v1/batches/$batchId") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get assert(response.getStatus === 404) assert(response.readEntity(classOf[String]).contains(msg)) @@ -622,6 +631,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "1") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() batchVersion match { case "1" => @@ -640,6 +650,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "1") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(logResponse.getStatus === 404) assert(logResponse.readEntity(classOf[String]).contains("Invalid batchId")) @@ -654,6 +665,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite .queryParam("from", "0") .queryParam("size", "1") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(logResponse.getStatus === 500) assert(logResponse.readEntity(classOf[String]).contains( @@ -676,13 +688,10 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite engineType = "SPARK") sessionManager.insertMetadata(metadata) - val encodeAuthorization = - new String(Base64.getEncoder.encode("kyuubi".getBytes()), "UTF-8") - // delete the batch in the same kyuubi instance but not found in-memory var deleteResp = webTarget.path(s"api/v1/batches/${metadata.identifier}") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("kyuubi")) .delete() assert(deleteResp.getStatus === 200) assert(!deleteResp.readEntity(classOf[CloseBatchResponse]).isSuccess) @@ -690,7 +699,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite // delete batch that is not existing deleteResp = webTarget.path(s"api/v1/batches/${UUID.randomUUID.toString}") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("kyuubi")) .delete() assert(deleteResp.getStatus === 404) assert(deleteResp.readEntity(classOf[String]).contains("Invalid batchId:")) @@ -703,7 +712,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite // delete batch that need make redirection deleteResp = webTarget.path(s"api/v1/batches/${metadata2.identifier}") .request(MediaType.APPLICATION_JSON_TYPE) - .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("kyuubi")) .delete() assert(deleteResp.getStatus === 200) assert(deleteResp.readEntity(classOf[CloseBatchResponse]).getMsg.contains( @@ -718,6 +727,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val response = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .header(conf.get(FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER), realClientIp) .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) assert(response.getStatus === 200) @@ -748,6 +758,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite eventually(timeout(10.seconds)) { val response = webTarget.path("api/v1/batches") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) assert(response.getStatus === 200) val batch = response.readEntity(classOf[Batch]) @@ -763,6 +774,7 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val deleteResp = webTarget.path(s"api/v1/batches/$batchId") .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .delete() assert(deleteResp.getStatus === 200) @@ -824,10 +836,51 @@ abstract class BatchesResourceSuiteBase extends KyuubiFunSuite val response = webTarget.path("api/v1/batches") .queryParam("batchName", uniqueName) .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) .get() assert(response.getStatus == 200) val getBatchListResponse = response.readEntity(classOf[GetBatchesResponse]) assert(getBatchListResponse.getTotal == 1) } + + test("open batch session with proxyUser") { + val normalUser = "kyuubi" + + def runOpenBatchExecutor( + kyuubiProxyUser: Option[String], + hs2ProxyUser: Option[String]): Response = { + val conf = mutable.Map("spark.master" -> "local") + + kyuubiProxyUser.map { username => + conf += (PROXY_USER.key -> username) + } + hs2ProxyUser.map { username => + conf += (KyuubiAuthenticationFactory.HS2_PROXY_USER -> username) + } + val proxyUserRequest = newSparkBatchRequest(conf.toMap) + + webTarget.path("api/v1/batches") + .request(MediaType.APPLICATION_JSON_TYPE) + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("anonymous")) + .post(Entity.entity(proxyUserRequest, MediaType.APPLICATION_JSON_TYPE)) + } + + // use kyuubi.session.proxy.user + val proxyUserResponse1 = runOpenBatchExecutor(Option(normalUser), None) + assert(proxyUserResponse1.getStatus === 405) + val errorMessage = s"Failed to validate proxy privilege of anonymous for $normalUser" + assert(proxyUserResponse1.readEntity(classOf[String]).contains(errorMessage)) + + // it should be the same behavior as hive.server2.proxy.user + val proxyUserResponse2 = runOpenBatchExecutor(None, Option(normalUser)) + assert(proxyUserResponse2.getStatus === 405) + assert(proxyUserResponse2.readEntity(classOf[String]).contains(errorMessage)) + + // when both set, kyuubi.session.proxy.user takes precedence + val proxyUserResponse3 = + runOpenBatchExecutor(Option(normalUser), Option(s"${normalUser}HiveServer2")) + assert(proxyUserResponse3.getStatus === 405) + assert(proxyUserResponse3.readEntity(classOf[String]).contains(errorMessage)) + } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/OperationsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/OperationsResourceSuite.scala index 72cd4d87db1..c4d67ad6211 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/OperationsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/OperationsResourceSuite.scala @@ -23,16 +23,16 @@ import javax.ws.rs.core.MediaType import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 import org.scalatest.concurrent.PatienceConfiguration.Timeout import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.{KyuubiFunSuite, RestFrontendTestHelper} +import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto._ import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.events.KyuubiOperationEvent import org.apache.kyuubi.operation.{ExecuteStatement, OperationState} import org.apache.kyuubi.operation.OperationState.{FINISHED, OperationState} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 class OperationsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { @@ -205,6 +205,23 @@ class OperationsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper assert(logRowSet.getRowCount == 1) } + test("support to return operation progress for REST api") { + val sessionHandle = fe.be.openSession( + HIVE_CLI_SERVICE_PROTOCOL_V2, + "admin", + "123456", + "localhost", + Map(KyuubiConf.SESSION_PROGRESS_ENABLE.key -> "true")) + val op = fe.be.executeStatement(sessionHandle, "show tables", Map.empty, runAsync = true, 3000) + eventually(Timeout(5.seconds)) { + val response = webTarget.path(s"api/v1/operations/${op.identifier}/event") + .request(MediaType.APPLICATION_JSON_TYPE).get() + assert(response.getStatus === 200) + val operationEvent = response.readEntity(classOf[dto.KyuubiOperationEvent]) + assert(operationEvent.getProgress != null) + } + } + def getOpHandleStr(statement: String = "show tables"): String = { val sessionHandle = fe.be.openSession( HIVE_CLI_SERVICE_PROTOCOL_V2, @@ -228,8 +245,8 @@ class OperationsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper val response = webTarget.path(s"api/v1/operations/$opHandleStr/event") .request(MediaType.APPLICATION_JSON_TYPE).get() assert(response.getStatus === 200) - val operationEvent = response.readEntity(classOf[KyuubiOperationEvent]) - assert(operationEvent.state === state.name()) + val operationEvent = response.readEntity(classOf[dto.KyuubiOperationEvent]) + assert(operationEvent.getState === state.name()) } } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index b58e87bc8c2..af49598fe82 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -35,7 +35,7 @@ import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_CONNECTION_URL import org.apache.kyuubi.engine.ShareLevel import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.operation.OperationHandle -import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER +import org.apache.kyuubi.server.http.util.HttpAuthUtils.{basicAuthorizationHeader, AUTHORIZATION_HEADER} import org.apache.kyuubi.session.SessionType class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { @@ -62,8 +62,11 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val statistic = webTarget.path("api/v1/sessions/execPool/statistic").request().get() val execPoolStatistic1 = statistic.readEntity(classOf[ExecPoolStatistic]) + // because this operation is asynchronous, + // there is no guarantee that it will complete quickly or fail in the process + // so we can not guarantee the poolActiveThread count must equal to 1 assert(execPoolStatistic1.getExecPoolSize == 1 && - execPoolStatistic1.getExecPoolActiveCount == 1) + execPoolStatistic1.getExecPoolActiveCount <= 1) response = webTarget.path("api/v1/sessions/count").request().get() val openedSessionCount = response.readEntity(classOf[SessionOpenCount]) @@ -97,23 +100,23 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { response = webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() assert(200 == response.getStatus) - // get session list again - response2 = webTarget.path("api/v1/sessions").request().get() - assert(200 == response2.getStatus) - val sessions2 = response2.readEntity(classOf[Seq[SessionData]]) - assert(sessions2.isEmpty) + // because delete is a asynchronous operation, we need eventually to + // make sure the delete operation process complete + eventually(timeout(3.seconds)) { + // get session list again + response2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == response2.getStatus) + + val sessions = response2.readEntity(classOf[Seq[SessionData]]) + assert(sessions.isEmpty) + } } test("get session event") { val sessionOpenRequest = new SessionOpenRequest(Map("testConfig" -> "testValue").asJava) - - val user = "kyuubi".getBytes() - val sessionOpenResp = webTarget.path("api/v1/sessions") .request(MediaType.APPLICATION_JSON_TYPE) - .header( - AUTHORIZATION_HEADER, - s"Basic ${new String(Base64.getEncoder().encode(user), StandardCharsets.UTF_8)}") + .header(AUTHORIZATION_HEADER, basicAuthorizationHeader("kyuubi")) .post(Entity.entity(sessionOpenRequest, MediaType.APPLICATION_JSON_TYPE)) val sessionHandle = sessionOpenResp.readEntity(classOf[SessionHandle]).getIdentifier diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/AdminRestApiSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/AdminRestApiSuite.scala index d63e4660772..c8f1d68e67e 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/AdminRestApiSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/AdminRestApiSuite.scala @@ -21,7 +21,6 @@ import java.util.UUID import scala.collection.JavaConverters.asScalaBufferConverter -import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 import org.mockito.Mockito.lenient import org.scalatestplus.mockito.MockitoSugar.mock @@ -33,6 +32,7 @@ import org.apache.kyuubi.ha.HighAvailabilityConf import org.apache.kyuubi.ha.client.{DiscoveryPaths, ServiceDiscovery} import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient import org.apache.kyuubi.plugin.PluginLoader +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 class AdminRestApiSuite extends RestClientTestHelper { test("refresh kyuubi server hadoop conf") { @@ -74,7 +74,7 @@ class AdminRestApiSuite extends RestClientTestHelper { .build() val adminRestApi = new AdminRestApi(basicKyuubiRestClient) - var engines = adminRestApi.listEngines("spark_sql", "user", "default", "", "false").asScala + var engines = adminRestApi.listEngines("spark_sql", "user", "default", "").asScala assert(engines.size == 1) assert(engines(0).getUser == user) assert(engines(0).getVersion == KYUUBI_VERSION) @@ -87,7 +87,7 @@ class AdminRestApiSuite extends RestClientTestHelper { val result = adminRestApi.deleteEngine("spark_sql", "user", "default", "") assert(result == s"Engine ${engineSpace} is deleted successfully.") - engines = adminRestApi.listEngines("spark_sql", "user", "default", "", "false").asScala + engines = adminRestApi.listEngines("spark_sql", "user", "default", "").asScala assert(engines.isEmpty) } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchCliSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchCliSuite.scala index bcf8c450eb8..4d5e352f182 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchCliSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchCliSuite.scala @@ -25,7 +25,6 @@ import java.util.UUID import org.apache.hadoop.security.UserGroupInformation import org.apache.hadoop.shaded.com.nimbusds.jose.util.StandardCharset -import org.apache.hive.service.rpc.thrift.TProtocolVersion import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import org.apache.kyuubi.{BatchTestHelper, RestClientTestHelper, Utils} @@ -36,6 +35,7 @@ import org.apache.kyuubi.engine.ApplicationManagerInfo import org.apache.kyuubi.metrics.{MetricsConstants, MetricsSystem} import org.apache.kyuubi.server.metadata.api.MetadataFilter import org.apache.kyuubi.session.KyuubiSessionManager +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class BatchCliSuite extends RestClientTestHelper with TestPrematureExit with BatchTestHelper { @@ -83,7 +83,7 @@ class BatchCliSuite extends RestClientTestHelper with TestPrematureExit with Bat | resource: ${sparkBatchTestResource.get} | className: org.apache.spark.examples.DriverSubmissionTest | args: - | - 10 + | - 120 | configs: | spark.master: local | wait.completion: true @@ -147,14 +147,20 @@ class BatchCliSuite extends RestClientTestHelper with TestPrematureExit with Bat "batch", batchId, "--size", - "2", + "100", "--username", ldapUser, "--password", ldapUserPasswd) - result = testPrematureExitForControlCli(logArgs, "") - val rows = result.split("\n") - assert(rows.length == 2) + eventually(timeout(60.seconds), interval(100.milliseconds)) { + invalidCount += 1 + result = testPrematureExitForControlCli(logArgs, "") + val rows = result.split("\n") + assert(rows.length >= 2) + // org.apache.spark.examples.DriverSubmissionTest output + assert(result.contains("Alive for")) + invalidCount -= 1 + } val deleteArgs = Array( "delete", @@ -168,7 +174,7 @@ class BatchCliSuite extends RestClientTestHelper with TestPrematureExit with Bat eventually(timeout(3.seconds), interval(200.milliseconds)) { assert(MetricsSystem.counterValue( - MetricsConstants.REST_CONN_TOTAL).getOrElse(0L) - totalConnections - invalidCount === 5) + MetricsConstants.REST_CONN_TOTAL).getOrElse(0L) - totalConnections - invalidCount >= 5) assert(MetricsSystem.counterValue(MetricsConstants.REST_CONN_OPEN).getOrElse(0L) === 0) } } @@ -206,12 +212,16 @@ class BatchCliSuite extends RestClientTestHelper with TestPrematureExit with Bat "batch", batchId, "--size", - "2", + "100", "--authSchema", "spnego") - result = testPrematureExitForControlCli(logArgs, "") - val rows = result.split("\n") - assert(rows.length == 2) + eventually(timeout(60.seconds), interval(100.milliseconds)) { + result = testPrematureExitForControlCli(logArgs, "") + val rows = result.split("\n") + assert(rows.length >= 2) + // org.apache.spark.examples.DriverSubmissionTest output + assert(result.contains("Alive for")) + } val deleteArgs = Array( "delete", diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchRestApiSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchRestApiSuite.scala index d04826a9d20..20ec2fc0a5f 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchRestApiSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/BatchRestApiSuite.scala @@ -74,16 +74,9 @@ class BatchRestApiSuite extends RestClientTestHelper with BatchTestHelper { } // delete batch - val closeResp = batchRestApi.deleteBatch(batch.getId(), null) + val closeResp = batchRestApi.deleteBatch(batch.getId()) assert(closeResp.getMsg.nonEmpty) - // delete batch - error - val e = intercept[KyuubiRestException] { - batchRestApi.deleteBatch(batch.getId(), "fake") - } - assert(e.getCause.toString.contains( - s"Failed to validate proxy privilege of ${ldapUser} for fake")) - basicKyuubiRestClient.close() } @@ -170,7 +163,7 @@ class BatchRestApiSuite extends RestClientTestHelper with BatchTestHelper { } // delete batch - val closeResp = batchRestApi.deleteBatch(batch.getId(), proxyUser) + val closeResp = batchRestApi.deleteBatch(batch.getId()) assert(closeResp.getMsg.nonEmpty) // list batches diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/OperationRestApiSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/OperationRestApiSuite.scala index fed685c4478..e02cfd260c3 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/OperationRestApiSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/OperationRestApiSuite.scala @@ -19,7 +19,6 @@ package org.apache.kyuubi.server.rest.client import scala.collection.JavaConverters._ -import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 import org.scalatest.concurrent.PatienceConfiguration.Timeout import org.scalatest.time.SpanSugar.convertIntToGrainOfTime @@ -28,6 +27,7 @@ import org.apache.kyuubi.client.{KyuubiRestClient, OperationRestApi} import org.apache.kyuubi.client.api.v1.dto.OpActionRequest import org.apache.kyuubi.client.exception.KyuubiRestException import org.apache.kyuubi.operation.OperationState +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V2 class OperationRestApiSuite extends RestClientTestHelper { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala index 5d219de33cc..fb43fcf8169 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala @@ -17,10 +17,9 @@ package org.apache.kyuubi.server.rest.client -import org.apache.hive.service.rpc.thrift.TProtocolVersion - import org.apache.kyuubi.RestClientTestHelper import org.apache.kyuubi.ctl.{CtlConf, TestPrematureExit} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion class SessionCtlSuite extends RestClientTestHelper with TestPrematureExit { override def beforeAll(): Unit = { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionRestApiSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionRestApiSuite.scala index a1dfd243229..8afb3ccad97 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionRestApiSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionRestApiSuite.scala @@ -23,7 +23,6 @@ import java.util.Collections import scala.collection.JavaConverters._ import scala.concurrent.duration.DurationInt -import org.apache.hive.service.rpc.thrift.TGetInfoType import org.scalatest.concurrent.PatienceConfiguration.Timeout import org.apache.kyuubi.RestClientTestHelper @@ -32,6 +31,7 @@ import org.apache.kyuubi.client.api.v1.dto._ import org.apache.kyuubi.client.exception.KyuubiRestException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.session.SessionType +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TGetInfoType class SessionRestApiSuite extends RestClientTestHelper { test("get/close/list/count session") { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/trino/api/TrinoContextSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/trino/api/TrinoContextSuite.scala index 6c5a01e4659..967a882d866 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/trino/api/TrinoContextSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/trino/api/TrinoContextSuite.scala @@ -24,7 +24,6 @@ import javax.ws.rs.core.MediaType import scala.collection.JavaConverters._ import io.trino.client.ProtocolHeaders.TRINO_HEADERS -import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V9 import org.scalatest.concurrent.PatienceConfiguration.Timeout import org.scalatest.time.SpanSugar.convertIntToGrainOfTime @@ -32,6 +31,7 @@ import org.apache.kyuubi.{KyuubiFunSuite, RestFrontendTestHelper} import org.apache.kyuubi.events.KyuubiOperationEvent import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle} import org.apache.kyuubi.operation.OperationState.{FINISHED, OperationState} +import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V9 class TrinoContextSuite extends KyuubiFunSuite with RestFrontendTestHelper { diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/service/CheckServerSPISuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/service/CheckServerSPISuite.scala new file mode 100644 index 00000000000..295c7df3de9 --- /dev/null +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/service/CheckServerSPISuite.scala @@ -0,0 +1,42 @@ +/* + * 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.kyuubi.service + +import java.nio.file.Paths + +// scalastyle:off +import org.scalatest.funsuite.AnyFunSuite + +import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ + +class CheckServerSPISuite extends AnyFunSuite { + // scalastyle:on + + test("check server SPI service file sorted") { + Seq( + "org.apache.hadoop.security.token.TokenIdentifier", + "org.apache.kyuubi.credentials.HadoopDelegationTokenProvider", + "org.apache.kyuubi.engine.ApplicationOperation") + .foreach { fileName => + val filePath = Paths.get( + s"${getCurrentModuleHome(this)}/src/main/resources/META-INF/services/$fileName") + assertFileContentSorted(filePath) + } + } +} diff --git a/kyuubi-server/web-ui/.env b/kyuubi-server/web-ui/.env new file mode 100644 index 00000000000..fb092780fc0 --- /dev/null +++ b/kyuubi-server/web-ui/.env @@ -0,0 +1,16 @@ + # 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. + +VITE_APP_VERSION=$npm_package_version diff --git a/kyuubi-server/web-ui/.env.production b/kyuubi-server/web-ui/.env.production index 1781b580123..9d442ad4523 100644 --- a/kyuubi-server/web-ui/.env.production +++ b/kyuubi-server/web-ui/.env.production @@ -13,6 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -NODE_ENV=production - VITE_APP_DEV_WEB_URL='/' diff --git a/kyuubi-server/web-ui/package-lock.json b/kyuubi-server/web-ui/package-lock.json index fa01c240573..3dab868017f 100644 --- a/kyuubi-server/web-ui/package-lock.json +++ b/kyuubi-server/web-ui/package-lock.json @@ -9,11 +9,13 @@ "version": "1.9.0-SNAPSHOT", "dependencies": { "@element-plus/icons-vue": "^2.0.9", - "axios": "^0.27.2", + "axios": "^1.6.0", "date-fns": "^2.29.3", "element-plus": "^2.2.12", + "monaco-editor": "^0.44.0", "pinia": "^2.0.18", "pinia-plugin-persistedstate": "^2.1.1", + "sql-formatter": "^13.0.1", "swagger-ui-dist": "^4.9.1", "vue": "^3.2.37", "vue-i18n": "^9.2.2", @@ -1491,8 +1493,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-union": { "version": "2.1.0", @@ -1523,12 +1524,13 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -1715,6 +1717,11 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1911,6 +1918,11 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2571,6 +2583,17 @@ "node": "*" } }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3160,6 +3183,16 @@ "ufo": "^1.1.2" } }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3189,6 +3222,27 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3545,6 +3599,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -3586,6 +3645,23 @@ } ] }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -3636,6 +3712,14 @@ "node": ">=4" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3813,6 +3897,19 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "node_modules/sql-formatter": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-13.1.0.tgz", + "integrity": "sha512-/nZQXuN7KzipFNM20ko+dHY4kOr9rymSfZLUDED8rhx3m8OK5y74jcyN+y1L51ZqHqiB0kp40VdpZP99uWvQdA==", + "dependencies": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5492,8 +5589,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-union": { "version": "2.1.0", @@ -5518,12 +5614,13 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "balanced-match": { @@ -5668,6 +5765,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5821,6 +5923,11 @@ "path-type": "^4.0.0" } }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6306,6 +6413,11 @@ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==" + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6756,6 +6868,16 @@ "ufo": "^1.1.2" } }, + "monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" + }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6773,6 +6895,17 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6999,6 +7132,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7023,6 +7161,20 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7061,6 +7213,11 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7177,6 +7334,16 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "sql-formatter": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-13.1.0.tgz", + "integrity": "sha512-/nZQXuN7KzipFNM20ko+dHY4kOr9rymSfZLUDED8rhx3m8OK5y74jcyN+y1L51ZqHqiB0kp40VdpZP99uWvQdA==", + "requires": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + } + }, "stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/kyuubi-server/web-ui/package.json b/kyuubi-server/web-ui/package.json index 239c6270623..607fa4f3cd5 100644 --- a/kyuubi-server/web-ui/package.json +++ b/kyuubi-server/web-ui/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.0.9", - "axios": "^0.27.2", + "axios": "^1.6.0", "date-fns": "^2.29.3", "element-plus": "^2.2.12", "monaco-editor": "^0.44.0", diff --git a/kyuubi-server/web-ui/pnpm-lock.yaml b/kyuubi-server/web-ui/pnpm-lock.yaml index a83d162ab6c..14f50016028 100644 --- a/kyuubi-server/web-ui/pnpm-lock.yaml +++ b/kyuubi-server/web-ui/pnpm-lock.yaml @@ -1,12 +1,12 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' dependencies: '@element-plus/icons-vue': specifier: ^2.0.9 version: 2.0.9(vue@3.2.37) axios: - specifier: ^0.27.2 - version: 0.27.2 + specifier: ^1.6.0 + version: 1.6.0 date-fns: specifier: ^2.29.3 version: 2.29.3 @@ -1038,11 +1038,12 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + /axios@1.6.0: + resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} dependencies: follow-redirects: 1.15.1 form-data: 4.0.0 + proxy-from-env: 1.1.0 transitivePeerDependencies: - debug dev: false @@ -2308,6 +2309,10 @@ packages: react-is: 17.0.2 dev: true + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true diff --git a/kyuubi-server/web-ui/src/api/editor/index.ts b/kyuubi-server/web-ui/src/api/editor/index.ts new file mode 100644 index 00000000000..daaf0471c12 --- /dev/null +++ b/kyuubi-server/web-ui/src/api/editor/index.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +import request from '@/utils/request' +import type { + IOpenSessionRequest, + IRunSqlRequest, + IGetSqlRowsetRequest, + IGetSqlMetadataRequest +} from './types' + +export function openSession(data: IOpenSessionRequest): any { + return request({ + url: 'api/v1/sessions', + method: 'post', + data + }) +} + +export function closeSession(identifier: string): any { + return request({ + url: `api/v1/sessions/${identifier}`, + method: 'delete' + }) +} + +export function runSql(data: IRunSqlRequest, identifier: string): any { + return request({ + url: `api/v1/sessions/${identifier}/operations/statement`, + method: 'post', + data + }) +} + +export function getSqlRowset(params: IGetSqlRowsetRequest): any { + return request({ + url: `api/v1/operations/${params.operationHandleStr}/rowset`, + method: 'get', + params + }) +} + +export function getSqlMetadata(params: IGetSqlMetadataRequest): any { + return request({ + url: `api/v1/operations/${params.operationHandleStr}/resultsetmetadata`, + method: 'get', + params + }) +} + +export function getLog(identifier: string): any { + return request({ + url: `api/v1/operations/${identifier}/log`, + method: 'get' + }) +} + +export function closeOperation(identifier: string) { + return request({ + url: `api/v1/admin/operations/${identifier}`, + method: 'delete' + }) +} diff --git a/kyuubi-server/web-ui/src/api/editor/types.ts b/kyuubi-server/web-ui/src/api/editor/types.ts new file mode 100644 index 00000000000..0bc4c2086c6 --- /dev/null +++ b/kyuubi-server/web-ui/src/api/editor/types.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +interface IOpenSessionRequest { + 'kyuubi.engine.type': string +} + +interface IRunSqlRequest { + statement: string + runAsync: boolean +} + +interface IGetSqlRowsetRequest { + operationHandleStr: string + fetchorientation: 'FETCH_NEXT' + maxrows: number +} + +interface IGetSqlMetadataRequest { + operationHandleStr: string +} + +export { + IOpenSessionRequest, + IRunSqlRequest, + IGetSqlRowsetRequest, + IGetSqlMetadataRequest +} diff --git a/kyuubi-server/web-ui/src/api/server/index.ts b/kyuubi-server/web-ui/src/api/server/index.ts index e2d74d7dbaf..4dd402b67f7 100644 --- a/kyuubi-server/web-ui/src/api/server/index.ts +++ b/kyuubi-server/web-ui/src/api/server/index.ts @@ -17,7 +17,7 @@ import request from '@/utils/request' -export function getAllServer() { +export function getAllServer(): any { return request({ url: 'api/v1/admin/server', method: 'get' diff --git a/kyuubi-server/web-ui/src/api/server/types.ts b/kyuubi-server/web-ui/src/api/server/types.ts new file mode 100644 index 00000000000..c747f436007 --- /dev/null +++ b/kyuubi-server/web-ui/src/api/server/types.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +interface IServer { + attributes: any | null + host: string + instance: string + namespace: string + nodeName: string + port: number + status: string +} + +export { IServer } diff --git a/kyuubi-server/web-ui/src/assets/images/document.svg b/kyuubi-server/web-ui/src/assets/images/document.svg new file mode 100644 index 00000000000..e3d1bfe1beb --- /dev/null +++ b/kyuubi-server/web-ui/src/assets/images/document.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/kyuubi-server/web-ui/src/assets/images/kyuubi-logo.svg b/kyuubi-server/web-ui/src/assets/images/kyuubi-logo.svg new file mode 100644 index 00000000000..682bc80e768 --- /dev/null +++ b/kyuubi-server/web-ui/src/assets/images/kyuubi-logo.svg @@ -0,0 +1,126 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/kyuubi-server/web-ui/src/assets/kyuubi.png b/kyuubi-server/web-ui/src/assets/images/kyuubi.png similarity index 100% rename from kyuubi-server/web-ui/src/assets/kyuubi.png rename to kyuubi-server/web-ui/src/assets/images/kyuubi.png diff --git a/kyuubi-server/web-ui/src/components/monaco-editor/index.vue b/kyuubi-server/web-ui/src/components/monaco-editor/index.vue index 4c387172a1a..65a2dba3421 100644 --- a/kyuubi-server/web-ui/src/components/monaco-editor/index.vue +++ b/kyuubi-server/web-ui/src/components/monaco-editor/index.vue @@ -24,7 +24,7 @@ import * as monaco from 'monaco-editor' import { format } from 'sql-formatter' import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' - import { editorProps } from './type' + import { editorProps } from './types' import { useEditorStore } from '@/pinia/editor' import { ref, toRaw, watch, onBeforeUnmount, onMounted } from 'vue' @@ -88,7 +88,7 @@ editor = monaco.editor.create(codeEditBox.value, { value: props.modelValue, language: props.language, - theme: monacoEditorThemeRef.value, + theme: props.theme || monacoEditorThemeRef.value, ...props.options }) diff --git a/kyuubi-server/web-ui/src/components/monaco-editor/type.ts b/kyuubi-server/web-ui/src/components/monaco-editor/types.ts similarity index 93% rename from kyuubi-server/web-ui/src/components/monaco-editor/type.ts rename to kyuubi-server/web-ui/src/components/monaco-editor/types.ts index 80400565eb0..aa962d43c1a 100644 --- a/kyuubi-server/web-ui/src/components/monaco-editor/type.ts +++ b/kyuubi-server/web-ui/src/components/monaco-editor/types.ts @@ -53,10 +53,7 @@ export const editorProps = { default: 'sql' }, theme: { - type: String as PropType, - validator(value: string): boolean { - return ['vs', 'vs-dark'].includes(value) - }, + type: String as PropType, default: 'vs' }, options: { @@ -72,7 +69,7 @@ export const editorProps = { }, readOnly: false, contextmenu: true, - fontSize: 16, + fontSize: 14, scrollBeyondLastLine: true, overviewRulerBorder: false } diff --git a/kyuubi-server/web-ui/src/layout/components/aside/index.vue b/kyuubi-server/web-ui/src/layout/components/aside/index.vue index 52304abff1d..c5d1e41aeb5 100644 --- a/kyuubi-server/web-ui/src/layout/components/aside/index.vue +++ b/kyuubi-server/web-ui/src/layout/components/aside/index.vue @@ -18,8 +18,9 @@ @@ -37,34 +38,43 @@ const { isCollapse } = storeToRefs(store) const router = useRoute() const activePath = ref(router.path) + const version = import.meta.env.VITE_APP_VERSION diff --git a/kyuubi-server/web-ui/src/layout/components/aside/types.ts b/kyuubi-server/web-ui/src/layout/components/aside/types.ts index a7e495e187c..76bb1f387c6 100644 --- a/kyuubi-server/web-ui/src/layout/components/aside/types.ts +++ b/kyuubi-server/web-ui/src/layout/components/aside/types.ts @@ -43,61 +43,14 @@ export const MENUS = [ } ] }, - { - label: 'Workload', - icon: 'List', - children: [ - { - label: 'Analysis', - icon: 'VideoPlay', - router: '/workload/analysis' - }, - { - label: 'Queue', - icon: 'Select', - router: '/workload/queue' - }, - { - label: 'Session', - icon: 'Select', - router: '/workload/session' - }, - { - label: 'Query', - icon: 'Select', - router: '/workload/query' - } - ] - }, - { - label: 'Operation', - icon: 'List', - children: [ - { - label: 'Running Jobs', - icon: 'VideoPlay', - router: '/operation/runningJobs' - }, - { - label: 'Completed Jobs', - icon: 'Select', - router: '/operation/completedJobs' - } - ] - }, { label: 'Swagger', icon: 'List', router: '/swagger' }, { - label: 'Contact Us', - icon: 'PhoneFilled', - router: '/contact' - }, - { - label: 'SQL Lab', + label: 'SQL Editor', icon: 'Cpu', - router: '/lab' + router: '/editor' } ] diff --git a/kyuubi-server/web-ui/src/locales/en_US/index.ts b/kyuubi-server/web-ui/src/locales/en_US/index.ts index 8606c74da6f..9bb0144ff40 100644 --- a/kyuubi-server/web-ui/src/locales/en_US/index.ts +++ b/kyuubi-server/web-ui/src/locales/en_US/index.ts @@ -37,6 +37,11 @@ export default { engine_ui: 'Engine UI', failure_reason: 'Failure Reason', session_properties: 'Session Properties', + no_data: 'No data', + no_log: 'No log', + run_sql_tips: 'Run a SQL to get result', + result: 'Result', + log: 'Log', operation: { text: 'Operation', delete_confirm: 'Delete Confirm', @@ -44,7 +49,8 @@ export default { cancel_confirm: 'Cancel Confirm', close: 'Close', cancel: 'Cancel', - delete: 'Delete' + delete: 'Delete', + run: 'Run' }, message: { delete_succeeded: 'Delete {name} Succeeded', @@ -52,6 +58,10 @@ export default { close_succeeded: 'Close {name} Succeeded', close_failed: 'Close {name} Failed', cancel_succeeded: 'Cancel {name} Succeeded', - cancel_failed: 'Cancel {name} Failed' + cancel_failed: 'Cancel {name} Failed', + run_sql_failed: 'Run SQL Failed', + get_sql_log_failed: 'Get SQL Log Failed', + get_sql_result_failed: 'Get SQL Result Failed', + get_sql_metadata_failed: 'Get SQL Metadata Failed' } } diff --git a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts index 0c4cb66db34..198f379eccb 100644 --- a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts +++ b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts @@ -37,6 +37,11 @@ export default { engine_ui: 'Engine UI', failure_reason: 'ๅคฑ่ดฅๅŽŸๅ› ', session_properties: 'Session ๅ‚ๆ•ฐ', + no_data: 'ๆ— ๆ•ฐๆฎ', + no_log: 'ๆ— ๆ—ฅๅฟ—', + run_sql_tips: '่ฏท่ฟ่กŒSQL่Žทๅ–็ป“ๆžœ', + result: '็ป“ๆžœ', + log: 'ๆ—ฅๅฟ—', operation: { text: 'ๆ“ไฝœ', delete_confirm: '็กฎ่ฎคๅˆ ้™ค', @@ -44,7 +49,8 @@ export default { cancel_confirm: '็กฎ่ฎคๅ–ๆถˆ', close: 'ๅ…ณ้—ญ', cancel: 'ๅ–ๆถˆ', - delete: 'ๅˆ ้™ค' + delete: 'ๅˆ ้™ค', + run: '่ฟ่กŒ' }, message: { delete_succeeded: 'ๅˆ ้™ค {name} ๆˆๅŠŸ', @@ -52,6 +58,10 @@ export default { close_succeeded: 'ๅ…ณ้—ญ {name} ๆˆๅŠŸ', close_failed: 'ๅ…ณ้—ญ {name} ๅคฑ่ดฅ', cancel_succeeded: 'ๅ–ๆถˆ {name} ๆˆๅŠŸ', - cancel_failed: 'ๅ–ๆถˆ {name} ๅคฑ่ดฅ' + cancel_failed: 'ๅ–ๆถˆ {name} ๅคฑ่ดฅ', + run_sql_failed: '่ฟ่กŒSQLๅคฑ่ดฅ', + get_sql_log_failed: '่Žทๅ–SQLๆ—ฅๅฟ—ๅคฑ่ดฅ', + get_sql_result_failed: '่Žทๅ–SQL็ป“ๆžœๅคฑ่ดฅ', + get_sql_metadata_failed: '่Žทๅ–SQLๅ…ƒๆ•ฐๆฎๅคฑ่ดฅ' } } diff --git a/kyuubi-server/web-ui/src/router/lab/index.ts b/kyuubi-server/web-ui/src/router/editor/index.ts similarity index 89% rename from kyuubi-server/web-ui/src/router/lab/index.ts rename to kyuubi-server/web-ui/src/router/editor/index.ts index d78838079bf..9d4df889cca 100644 --- a/kyuubi-server/web-ui/src/router/lab/index.ts +++ b/kyuubi-server/web-ui/src/router/editor/index.ts @@ -17,9 +17,9 @@ const routes = [ { - path: '/lab', - name: 'lab', - component: () => import('@/views/lab/index.vue') + path: '/editor', + name: 'editor', + component: () => import('@/views/editor/index.vue') } ] diff --git a/kyuubi-server/web-ui/src/router/index.ts b/kyuubi-server/web-ui/src/router/index.ts index c59c5f28c7b..7bbe344460e 100644 --- a/kyuubi-server/web-ui/src/router/index.ts +++ b/kyuubi-server/web-ui/src/router/index.ts @@ -17,13 +17,10 @@ import { createRouter, createWebHistory } from 'vue-router' import overviewRoutes from './overview' -import workloadRoutes from './workload' -import operationRoutes from './operation' -import contactRoutes from './contact' import managementRoutes from './management' import detailRoutes from './detail' import swaggerRoutes from './swagger' -import labRoutes from './lab' +import editorRoutes from './editor' const routes = [ { @@ -40,13 +37,10 @@ const routes = [ redirect: 'overview', children: [ ...overviewRoutes, - ...workloadRoutes, - ...operationRoutes, ...managementRoutes, ...detailRoutes, ...swaggerRoutes, - ...contactRoutes, - ...labRoutes + ...editorRoutes ] } ] diff --git a/kyuubi-server/web-ui/src/router/workload/index.ts b/kyuubi-server/web-ui/src/router/workload/index.ts deleted file mode 100644 index 7d7b91a47e5..00000000000 --- a/kyuubi-server/web-ui/src/router/workload/index.ts +++ /dev/null @@ -1,41 +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. - */ - -const routes = [ - { - path: '/workload/analysis', - name: 'workload-analysis', - component: () => import('@/views/workload/analysis/index.vue') - }, - { - path: '/workload/queue', - name: 'workload-queue', - component: () => import('@/views/workload/queue/index.vue') - }, - { - path: '/workload/session', - name: 'workload-session', - component: () => import('@/views/workload/session/index.vue') - }, - { - path: '/workload/query', - name: 'workload-query', - component: () => import('@/views/workload/query/index.vue') - } -] - -export default routes diff --git a/kyuubi-server/web-ui/src/test/unit/views/layout/aside.spec.ts b/kyuubi-server/web-ui/src/test/unit/views/layout/aside.spec.ts index 3e31535aef8..b7999ad173d 100644 --- a/kyuubi-server/web-ui/src/test/unit/views/layout/aside.spec.ts +++ b/kyuubi-server/web-ui/src/test/unit/views/layout/aside.spec.ts @@ -34,5 +34,5 @@ test('mount component', () => { plugins: [mockRouter, getStore()] } }) - expect(wrapper.text()).toContain('Apache Kyuubi Dashboard') + expect(wrapper.text()).toContain(import.meta.env.VITE_APP_VERSION) }) diff --git a/kyuubi-server/web-ui/src/views/editor/components/Editor.vue b/kyuubi-server/web-ui/src/views/editor/components/Editor.vue new file mode 100644 index 00000000000..21faee5a33a --- /dev/null +++ b/kyuubi-server/web-ui/src/views/editor/components/Editor.vue @@ -0,0 +1,290 @@ + + + + + + + diff --git a/kyuubi-server/web-ui/src/views/workload/analysis/index.vue b/kyuubi-server/web-ui/src/views/editor/components/Log.vue similarity index 55% rename from kyuubi-server/web-ui/src/views/workload/analysis/index.vue rename to kyuubi-server/web-ui/src/views/editor/components/Log.vue index 31b42d46ede..d2a403d9e54 100644 --- a/kyuubi-server/web-ui/src/views/workload/analysis/index.vue +++ b/kyuubi-server/web-ui/src/views/editor/components/Log.vue @@ -17,13 +17,40 @@ --> - - + diff --git a/kyuubi-server/web-ui/src/views/editor/components/Result.vue b/kyuubi-server/web-ui/src/views/editor/components/Result.vue new file mode 100644 index 00000000000..de103ff13af --- /dev/null +++ b/kyuubi-server/web-ui/src/views/editor/components/Result.vue @@ -0,0 +1,144 @@ + + + + + + + diff --git a/kyuubi-server/web-ui/src/views/editor/components/types.ts b/kyuubi-server/web-ui/src/views/editor/components/types.ts new file mode 100644 index 00000000000..42475bf4ae8 --- /dev/null +++ b/kyuubi-server/web-ui/src/views/editor/components/types.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +interface IResponse { + identifier: string +} + +interface ISqlResult { + dataName?: string + dataType: string + value: any +} + +interface IFields { + fields: ISqlResult[] +} + +interface ILog { + logRowSet: string[] + rowCount: number +} + +interface IErrorMessage { + title: string + description: string +} + +interface IError extends Error { + response?: { + data?: { + message?: string + } + } +} + +export { IResponse, ISqlResult, IFields, ILog, IErrorMessage, IError } diff --git a/kyuubi-server/web-ui/src/views/editor/index.vue b/kyuubi-server/web-ui/src/views/editor/index.vue new file mode 100644 index 00000000000..424d3e929c8 --- /dev/null +++ b/kyuubi-server/web-ui/src/views/editor/index.vue @@ -0,0 +1,141 @@ + + + + + + + diff --git a/kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss b/kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss new file mode 100644 index 00000000000..9027ef69a3a --- /dev/null +++ b/kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss @@ -0,0 +1,27 @@ +/* + * 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. + */ + +@mixin sharedNoData { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 14px; + color: #999; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + text-align: center; +} \ No newline at end of file diff --git a/kyuubi-server/web-ui/src/views/lab/index.vue b/kyuubi-server/web-ui/src/views/lab/index.vue deleted file mode 100644 index 26ecfac0d87..00000000000 --- a/kyuubi-server/web-ui/src/views/lab/index.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - diff --git a/kyuubi-server/web-ui/src/views/operation/completedJobs/index.vue b/kyuubi-server/web-ui/src/views/operation/completedJobs/index.vue deleted file mode 100644 index 7b587c4fa6e..00000000000 --- a/kyuubi-server/web-ui/src/views/operation/completedJobs/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/kyuubi-server/web-ui/src/views/operation/runningJobs/index.vue b/kyuubi-server/web-ui/src/views/operation/runningJobs/index.vue deleted file mode 100644 index 030b48ae9d8..00000000000 --- a/kyuubi-server/web-ui/src/views/operation/runningJobs/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/kyuubi-server/web-ui/src/views/overview/index.vue b/kyuubi-server/web-ui/src/views/overview/index.vue index 3a2334c8e6b..a363d0e9560 100644 --- a/kyuubi-server/web-ui/src/views/overview/index.vue +++ b/kyuubi-server/web-ui/src/views/overview/index.vue @@ -20,7 +20,7 @@
    - +
    @@ -30,20 +30,7 @@ import { reactive } from 'vue' import cCard from '@/components/card/index.vue' - const cards = reactive([ - { - title: 'Opened Session', - value: 1 - }, - { - title: 'ExecPool Size', - value: 2 - }, - { - title: 'ExecPool ActiveCount', - value: 3 - } - ]) + const cards = reactive([]) diff --git a/kyuubi-server/web-ui/src/views/swagger/index.vue b/kyuubi-server/web-ui/src/views/swagger/index.vue index 7f8fb7f99a8..1ff671d1f14 100644 --- a/kyuubi-server/web-ui/src/views/swagger/index.vue +++ b/kyuubi-server/web-ui/src/views/swagger/index.vue @@ -18,7 +18,6 @@ diff --git a/kyuubi-server/web-ui/src/views/workload/query/index.vue b/kyuubi-server/web-ui/src/views/workload/query/index.vue deleted file mode 100644 index 45d0cd91b42..00000000000 --- a/kyuubi-server/web-ui/src/views/workload/query/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/kyuubi-server/web-ui/src/views/workload/queue/index.vue b/kyuubi-server/web-ui/src/views/workload/queue/index.vue deleted file mode 100644 index bbeb8e985e9..00000000000 --- a/kyuubi-server/web-ui/src/views/workload/queue/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/kyuubi-server/web-ui/src/views/workload/session/index.vue b/kyuubi-server/web-ui/src/views/workload/session/index.vue deleted file mode 100644 index bd4ec51d58e..00000000000 --- a/kyuubi-server/web-ui/src/views/workload/session/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala b/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala new file mode 100644 index 00000000000..91327223a60 --- /dev/null +++ b/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala @@ -0,0 +1,75 @@ +/* + * 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.kyuubi.util.command + +import java.io.File + +import scala.util.matching.Regex + +object CommandLineUtils { + val CONF = "--conf" + + val PATTERN_FOR_KEY_VALUE_ARG: Regex = "(.+?)=(.+)".r + + val REDACTION_REPLACEMENT_TEXT = "*********(redacted)" + + /** + * The Java command's option name for classpath + */ + val CP = "-cp" + + /** + * Assemble key value pair with "=" seperator + */ + def genKeyValuePair(key: String, value: String): String = s"$key=$value".trim + + /** + * Assemble key value pair with config option prefix + */ + def confKeyValue(key: String, value: String, confOption: String = CONF): Iterable[String] = + Seq(confOption, genKeyValuePair(key, value)) + + def confKeyValueStr(key: String, value: String, confOption: String = CONF): String = + confKeyValue(key, value, confOption).mkString(" ") + + def confKeyValues(configs: Iterable[(String, String)]): Iterable[String] = + configs.flatMap { case (k, v) => confKeyValue(k, v) }.toSeq + + /** + * Generate classpath option by assembling the classpath entries with "-cp" prefix + */ + def genClasspathOption(classpathEntries: Iterable[String]): Iterable[String] = + Seq(CP, classpathEntries.mkString(File.pathSeparator)) + + /** + * Match the conf string in the form of "key=value" + * and redact the value with the replacement text if keys are contained in given config keys + */ + def redactConfValues( + commands: Iterable[String], + redactKeys: Iterable[String]): Iterable[String] = { + redactKeys.toSet match { + case redactKeySet if redactKeySet.isEmpty => commands + case redactKeySet => commands.map { + case PATTERN_FOR_KEY_VALUE_ARG(key, _) if redactKeySet.contains(key) => + genKeyValuePair(key, REDACTION_REPLACEMENT_TEXT) + case part => part + } + } + } +} diff --git a/kyuubi-util-scala/src/test/java/org/apache/kyuubi/tags/GlutenTest.java b/kyuubi-util-scala/src/test/java/org/apache/kyuubi/tags/GlutenTest.java new file mode 100644 index 00000000000..8620df4b95a --- /dev/null +++ b/kyuubi-util-scala/src/test/java/org/apache/kyuubi/tags/GlutenTest.java @@ -0,0 +1,29 @@ +/* + * 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.kyuubi.tags; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.scalatest.TagAnnotation; + +@TagAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface GlutenTest {} diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala index 9d33993b9d2..fc7d0db7ab9 100644 --- a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala @@ -17,9 +17,10 @@ package org.apache.kyuubi.util import java.nio.charset.StandardCharsets -import java.nio.file.Path +import java.nio.file.{Files, Path} import java.util.Locale +import scala.collection.JavaConverters._ import scala.collection.Traversable import scala.io.Source import scala.reflect.ClassTag @@ -29,6 +30,8 @@ import org.scalactic.Prettifier import org.scalactic.source.Position import org.scalatest.Assertions._ +import org.apache.kyuubi.util.GoldenFileUtils.getLicenceContent + object AssertionUtils { def assertEqualsIgnoreCase(expected: AnyRef)(actual: AnyRef)( @@ -106,6 +109,24 @@ object AssertionUtils { } } + def assertFileContentSorted( + filePath: Path, + headerSkipPrefix: String = "#", + licenceHeader: Iterable[String] = getLicenceContent(), + distinct: Boolean = true): Unit = { + val sortedLines = Files.readAllLines(filePath).asScala + .dropWhile(line => line.trim == "" || line.trim.startsWith(headerSkipPrefix)) + .map(_.trim).filter(_.nonEmpty) + .sorted + val expectedSortedLines = if (distinct) { + sortedLines.distinct + } else { + sortedLines + } + val expectedLines = licenceHeader ++ Seq("") ++ expectedSortedLines + assertFileContent(filePath, expectedLines, s"Check SPI provider file sorted $filePath") + } + /** * Assert the iterable contains all the expected elements */ @@ -151,7 +172,7 @@ object AssertionUtils { /** * Asserts that the given function throws an exception of the given type - * and with the exception message equals to expected string + * and with the exception message contains expected string */ def interceptContains[T <: Exception](f: => Any)(contained: String)(implicit classTag: ClassTag[T], @@ -160,4 +181,16 @@ object AssertionUtils { val exception = intercept[T](f)(classTag, pos) assert(exception.getMessage.contains(contained)) } + + /** + * Asserts that the given function throws an exception of the given type + * and with the exception message ends with expected string + */ + def interceptEndsWith[T <: Exception](f: => Any)(end: String)(implicit + classTag: ClassTag[T], + pos: Position): Unit = { + assert(end != null) + val exception = intercept[T](f)(classTag, pos) + assert(exception.getMessage.endsWith(end)) + } } diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala index e9927f7e23e..0ab292c9ced 100644 --- a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala @@ -48,4 +48,34 @@ object GoldenFileUtils { assertFileContent(path, lines, regenScript) } } + + def getCurrentModuleHome(obj: Any): String = { + obj.getClass.getProtectionDomain.getCodeSource.getLocation.getPath + .split("target").head + } + + val apacheLicenceContent: String = + """ 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. + |""".stripMargin + + def getLicenceContent( + header: String = "#", + linePrefix: String = "#", + footer: String = "#"): Iterable[String] = { + val content = apacheLicenceContent.split("\n").map(line => linePrefix + line) + Seq(header) ++ content ++ Seq(footer) + } } diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala new file mode 100644 index 00000000000..e000e7478b6 --- /dev/null +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala @@ -0,0 +1,50 @@ +/* + * 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.kyuubi.util.command +// scalastyle:off +import org.scalatest.funsuite.AnyFunSuite + +import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.command.CommandLineUtils._ + +// scalastyle:off +class CommandUtilsSuite extends AnyFunSuite { +// scalastyle:on + + test("assemble key value pair") { + assertResult("abc=123")(genKeyValuePair("abc", "123")) + assertResult("abc=123")(genKeyValuePair(" abc", "123 ")) + assertResult("abc.def=xyz.123")(genKeyValuePair("abc.def", "xyz.123")) + + assertMatches(genKeyValuePair("abc", "123"), PATTERN_FOR_KEY_VALUE_ARG) + assertMatches(genKeyValuePair(" abc", "123 "), PATTERN_FOR_KEY_VALUE_ARG) + assertMatches(genKeyValuePair("abc.def", "xyz.123"), PATTERN_FOR_KEY_VALUE_ARG) + } + + test("assemble key value pair with config option") { + assertResult("--conf abc=123")(confKeyValueStr("abc", "123")) + assertResult("--conf abc.def=xyz.123")(confKeyValueStr("abc.def", "xyz.123")) + + assertResult(Seq("--conf", "abc=123"))(confKeyValue("abc", "123")) + assertResult(Seq("--conf", "abc.def=xyz.123"))(confKeyValue("abc.def", "xyz.123")) + } + + test("assemble classpath options") { + assertResult(Seq("-cp", "/path/a.jar:/path2/b*.jar"))( + genClasspathOption(Seq("/path/a.jar", "/path2/b*.jar"))) + } +} diff --git a/kyuubi-zookeeper/pom.xml b/kyuubi-zookeeper/pom.xml index c4309fbab9b..eec201c2c61 100644 --- a/kyuubi-zookeeper/pom.xml +++ b/kyuubi-zookeeper/pom.xml @@ -38,7 +38,7 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} + ${kyuubi-relocated-zookeeper.artifacts} diff --git a/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeper.scala b/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeper.scala index 17caffedff6..1592d906313 100644 --- a/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeper.scala +++ b/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeper.scala @@ -19,9 +19,10 @@ package org.apache.kyuubi.zookeeper import java.io.File import java.net.InetSocketAddress +import java.nio.file.Paths import org.apache.kyuubi.Utils._ -import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.{ConfigEntry, KyuubiConf} import org.apache.kyuubi.service.{AbstractService, ServiceState} import org.apache.kyuubi.shaded.zookeeper.server.{NIOServerCnxnFactory, ZooKeeperServer} import org.apache.kyuubi.zookeeper.ZookeeperConf._ @@ -37,8 +38,9 @@ class EmbeddedZookeeper extends AbstractService("EmbeddedZookeeper") { private var host: String = _ override def initialize(conf: KyuubiConf): Unit = synchronized { - dataDirectory = new File(conf.get(ZK_DATA_DIR)) - dataLogDirectory = new File(conf.get(ZK_DATA_LOG_DIR)) + dataDirectory = resolvePathIfRelative(conf, ZK_DATA_DIR) + dataLogDirectory = resolvePathIfRelative(conf, ZK_DATA_LOG_DIR) + val clientPort = conf.get(ZK_CLIENT_PORT) val tickTime = conf.get(ZK_TICK_TIME) val maxClientCnxns = conf.get(ZK_MAX_CLIENT_CONNECTIONS) @@ -93,4 +95,10 @@ class EmbeddedZookeeper extends AbstractService("EmbeddedZookeeper") { assert(zks != null, s"$getName is in $getServiceState") s"$host:${serverFactory.getLocalPort}" } + + def resolvePathIfRelative(conf: KyuubiConf, configEntry: ConfigEntry[String]): File = { + val dirFromConfig = conf.get(configEntry) + Paths.get(sys.env.getOrElse(KyuubiConf.KYUUBI_HOME, ".")).resolve(dirFromConfig).toFile + } + } diff --git a/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/ZookeeperConf.scala b/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/ZookeeperConf.scala index 6ef494896a3..9b0844e6921 100644 --- a/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/ZookeeperConf.scala +++ b/kyuubi-zookeeper/src/main/scala/org/apache/kyuubi/zookeeper/ZookeeperConf.scala @@ -31,7 +31,8 @@ object ZookeeperConf { @deprecated("using kyuubi.zookeeper.embedded.data.dir instead", since = "1.2.0") val EMBEDDED_ZK_TEMP_DIR: ConfigEntry[String] = buildConf("kyuubi.zookeeper.embedded.directory") - .doc("The temporary directory for the embedded ZooKeeper server") + .doc("The temporary directory for the embedded ZooKeeper server. " + + "If it is a relative path, it is resolved relative to KYUUBI_HOME. ") .version("1.0.0") .stringConf .createWithDefault("embedded_zookeeper") @@ -58,12 +59,14 @@ object ZookeeperConf { val ZK_DATA_DIR: ConfigEntry[String] = buildConf("kyuubi.zookeeper.embedded.data.dir") .doc("dataDir for the embedded zookeeper server where stores the in-memory database" + - " snapshots and, unless specified otherwise, the transaction log of updates to the database.") + " snapshots and, unless specified otherwise, the transaction log of updates to the" + + " database. If it is a relative path, it is resolved relative to KYUUBI_HOME.") .version("1.2.0") .fallbackConf(EMBEDDED_ZK_TEMP_DIR) val ZK_DATA_LOG_DIR: ConfigEntry[String] = buildConf("kyuubi.zookeeper.embedded.data.log.dir") - .doc("dataLogDir for the embedded ZooKeeper server where writes the transaction log .") + .doc("dataLogDir for the embedded ZooKeeper server where writes the transaction log. " + + "If it is a relative path, it is resolved relative to KYUUBI_HOME.") .version("1.2.0") .fallbackConf(ZK_DATA_DIR) diff --git a/kyuubi-zookeeper/src/test/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeperSuite.scala b/kyuubi-zookeeper/src/test/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeperSuite.scala index 69e798ac538..8e1abda4feb 100644 --- a/kyuubi-zookeeper/src/test/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeperSuite.scala +++ b/kyuubi-zookeeper/src/test/scala/org/apache/kyuubi/zookeeper/EmbeddedZookeeperSuite.scala @@ -22,7 +22,7 @@ import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.shaded.curator.framework.CuratorFrameworkFactory import org.apache.kyuubi.shaded.curator.framework.imps.CuratorFrameworkState import org.apache.kyuubi.shaded.curator.retry.ExponentialBackoffRetry -import org.apache.kyuubi.zookeeper.ZookeeperConf.{ZK_CLIENT_PORT, ZK_CLIENT_PORT_ADDRESS} +import org.apache.kyuubi.zookeeper.ZookeeperConf.{ZK_CLIENT_PORT, ZK_CLIENT_PORT_ADDRESS, ZK_DATA_DIR, ZK_DATA_LOG_DIR} class EmbeddedZookeeperSuite extends KyuubiFunSuite { private var zkServer: EmbeddedZookeeper = _ @@ -64,4 +64,17 @@ class EmbeddedZookeeperSuite extends KyuubiFunSuite { zkServer.initialize(conf) assert(zkServer.getConnectString.contains("127.0.0.1")) } + + test("relative path from zookeeper config should be in kyuubi_home") { + zkServer = new EmbeddedZookeeper() + val conf = KyuubiConf() + .set(ZK_CLIENT_PORT, 0) + .set(ZK_DATA_LOG_DIR, "embedded_zookeeper_log") + .set(ZK_DATA_DIR, "/tmp/embedded_zookeeper_data") + + val dataDir = zkServer.resolvePathIfRelative(conf, ZK_DATA_DIR) + val dataLogDir = zkServer.resolvePathIfRelative(conf, ZK_DATA_LOG_DIR) + assert(dataDir.getAbsolutePath.equals("/tmp/embedded_zookeeper_data")) + assert(dataLogDir.getAbsolutePath.contains("/embedded_zookeeper_log")) + } } diff --git a/pom.xml b/pom.xml index b278d7be11c..f3b3d57644c 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ org.apache apache - 30 + 31 org.apache.kyuubi @@ -126,7 +126,7 @@ https://archive.apache.org/dist 2.3.0 1.67 - 4.2.8 + 4.2.23 1.5.0 1.15 3.2.2 @@ -173,22 +173,20 @@ 4.13.2 3.5.1 6.8.1 - kyuubi-shaded-zookeeper-34 - 0.1.0 + 0.2.0 + kyuubi-relocated-zookeeper-34 6.0.5 2.20.0 8.0.32 4.11.0 - - 4.1.93.Final + 4.1.100.Final 0.12.0 + 2.9.0 0.5.0-incubating ${spark.binary.version} 1.10.1 6.0.0 + 42.6.0 0.16.0 3.21.7 0.10.7 @@ -202,7 +200,7 @@ DO NOT forget to change the following properties when change the minor version of Spark: `delta.version`, `maven.plugin.scalatest.exclude.tags` --> - 3.4.1 + 3.4.2 3.4 spark-${spark.version}-bin-hadoop3${spark.archive.scala.suffix}.tgz @@ -233,7 +231,7 @@ ${project.build.directory}/scala-${scala.binary.version}/jars 3.3.0 - 1.6.8 + 1.7.1 1.6.1 1.12.1 @@ -243,7 +241,9 @@ false 2.30.0 - 0.8.7 + 3.2.1 + + 0.8.11 1.0.0 3.4.1 1.7.13 @@ -273,24 +273,6 @@ Apache Development Snapshot Repository https://repository.apache.org/content/repositories/snapshots - -XX:+IgnoreUnrecognizedVMOptions - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.lang.invoke=ALL-UNNAMED - --add-opens=java.base/java.lang.reflect=ALL-UNNAMED - --add-opens=java.base/java.io=ALL-UNNAMED - --add-opens=java.base/java.net=ALL-UNNAMED - --add-opens=java.base/java.nio=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - --add-opens=java.base/java.util.concurrent=ALL-UNNAMED - --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED - --add-opens=java.base/sun.nio.ch=ALL-UNNAMED - --add-opens=java.base/sun.nio.cs=ALL-UNNAMED - --add-opens=java.base/sun.security.action=ALL-UNNAMED - --add-opens=java.base/sun.security.tools.keytool=ALL-UNNAMED - --add-opens=java.base/sun.security.x509=ALL-UNNAMED - --add-opens=java.base/sun.util.calendar=ALL-UNNAMED - -Djdk.reflect.useDirectMethodHandle=false - -Dio.netty.tryReflectionSetAccessible=true -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 @@ -298,8 +280,13 @@ org.apache.kyuubi - ${kyuubi-shaded-zookeeper.artifacts} - ${kyuubi-shaded-zookeeper.version} + kyuubi-relocated-hive-service-rpc + ${kyuubi-relocated.version} + + + org.apache.kyuubi + ${kyuubi-relocated-zookeeper.artifacts} + ${kyuubi-relocated.version} org.antlr @@ -581,6 +568,18 @@ ${testcontainers-scala.version} + + com.dimafeng + testcontainers-scala-mysql_${scala.binary.version} + ${testcontainers-scala.version} + + + + com.dimafeng + testcontainers-scala-postgresql_${scala.binary.version} + ${testcontainers-scala.version} + + com.dimafeng testcontainers-scala-trino_${scala.binary.version} @@ -1351,6 +1350,12 @@ ${phoenix.version} + + org.postgresql + postgresql + ${postgresql.version} + + org.apache.flink @@ -1401,6 +1406,12 @@ provided + + org.apache.flink + flink-table-planner-loader + ${flink.version} + + org.apache.flink flink-sql-client @@ -1697,7 +1708,7 @@ true false - ${extraJavaTestArgs} + ${maven.plugin.surefire.argLine} @@ -1709,7 +1720,7 @@ ${project.build.directory}/surefire-reports . TestSuite.txt - ${extraJavaTestArgs} + ${maven.plugin.surefire.argLine} ${project.build.directory}/work @@ -2054,11 +2065,6 @@ spotless-maven-plugin - - org.jacoco - jacoco-maven-plugin - - org.apache.maven.plugins maven-antrun-plugin @@ -2135,6 +2141,37 @@ + + java-17 + + 17 + + + 17 + + + ${java.version} + -XX:+IgnoreUnrecognizedVMOptions + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/sun.nio.cs=ALL-UNNAMED + --add-opens=java.base/sun.security.action=ALL-UNNAMED + --add-opens=java.base/sun.security.tools.keytool=ALL-UNNAMED + --add-opens=java.base/sun.security.x509=ALL-UNNAMED + --add-opens=java.base/sun.util.calendar=ALL-UNNAMED + -Djdk.reflect.useDirectMethodHandle=false + -Dio.netty.tryReflectionSetAccessible=true + + + scala-2.13 @@ -2246,7 +2283,7 @@ delta-core 2.4.0 - 3.4.1 + 3.4.2 3.4 org.scalatest.tags.Slow @@ -2314,7 +2351,7 @@ zookeeper-3.6 - kyuubi-shaded-zookeeper-36 + kyuubi-relocated-zookeeper-36 @@ -2391,6 +2428,18 @@ + + codecov + + + + org.jacoco + jacoco-maven-plugin + + + + + apache-release